Browse Source

Create uwsgi-nginx

Stille 4 years ago
parent
commit
d6b54061b6
52 changed files with 2330 additions and 0 deletions
  1. 42 0
      .github/workflows/uwsgi-nginx.yml
  2. 112 0
      uwsgi-nginx/.gitignore
  3. 201 0
      uwsgi-nginx/LICENSE.txt
  4. 395 0
      uwsgi-nginx/README.md
  5. 41 0
      uwsgi-nginx/backup.travis.yml
  6. 67 0
      uwsgi-nginx/docker-images/Dockerfile
  7. 10 0
      uwsgi-nginx/docker-images/app/main.py
  8. 12 0
      uwsgi-nginx/docker-images/app/prestart.sh
  9. 2 0
      uwsgi-nginx/docker-images/app/uwsgi.ini
  10. 70 0
      uwsgi-nginx/docker-images/entrypoint.sh
  11. 95 0
      uwsgi-nginx/docker-images/install-nginx-alpine.sh
  12. 81 0
      uwsgi-nginx/docker-images/install-nginx-debian.sh
  13. 67 0
      uwsgi-nginx/docker-images/python2.7.dockerfile
  14. 67 0
      uwsgi-nginx/docker-images/python3.6.dockerfile
  15. 67 0
      uwsgi-nginx/docker-images/python3.7.dockerfile
  16. 72 0
      uwsgi-nginx/docker-images/python3.8-alpine.dockerfile
  17. 67 0
      uwsgi-nginx/docker-images/python3.8.dockerfile
  18. 15 0
      uwsgi-nginx/docker-images/start.sh
  19. 18 0
      uwsgi-nginx/docker-images/supervisord-alpine.ini
  20. 18 0
      uwsgi-nginx/docker-images/supervisord-debian.conf
  21. 10 0
      uwsgi-nginx/docker-images/uwsgi.ini
  22. 3 0
      uwsgi-nginx/mypy.ini
  23. 21 0
      uwsgi-nginx/pyproject.toml
  24. 7 0
      uwsgi-nginx/scripts/build-push-all.sh
  25. 15 0
      uwsgi-nginx/scripts/build-push.sh
  26. 12 0
      uwsgi-nginx/scripts/build.sh
  27. 5 0
      uwsgi-nginx/scripts/docker-login.sh
  28. 6 0
      uwsgi-nginx/scripts/format-imports.sh
  29. 7 0
      uwsgi-nginx/scripts/format.sh
  30. 8 0
      uwsgi-nginx/scripts/lint.sh
  31. 54 0
      uwsgi-nginx/scripts/process_all.py
  32. 4 0
      uwsgi-nginx/scripts/test-all.sh
  33. 5 0
      uwsgi-nginx/scripts/test.sh
  34. 1 0
      uwsgi-nginx/tests/.gitignore
  35. 0 0
      uwsgi-nginx/tests/__init__.py
  36. 0 0
      uwsgi-nginx/tests/test_01_main/__init__.py
  37. 78 0
      uwsgi-nginx/tests/test_01_main/test_defaults.py
  38. 90 0
      uwsgi-nginx/tests/test_01_main/test_env_vars_1.py
  39. 0 0
      uwsgi-nginx/tests/test_02_app/__init__.py
  40. 14 0
      uwsgi-nginx/tests/test_02_app/app_with_installs/app/main.py
  41. 0 0
      uwsgi-nginx/tests/test_02_app/custom_app/application/custom_app/__init__.py
  42. 10 0
      uwsgi-nginx/tests/test_02_app/custom_app/application/custom_app/main.py
  43. 2 0
      uwsgi-nginx/tests/test_02_app/custom_app/application/custom_app/uwsgi.ini
  44. 2 0
      uwsgi-nginx/tests/test_02_app/custom_app/prestart.sh
  45. 10 0
      uwsgi-nginx/tests/test_02_app/custom_nginx_app/app/main.py
  46. 25 0
      uwsgi-nginx/tests/test_02_app/custom_nginx_app/app/nginx.conf
  47. 10 0
      uwsgi-nginx/tests/test_02_app/simple_app/app/main.py
  48. 91 0
      uwsgi-nginx/tests/test_02_app/test_app_and_env_vars.py
  49. 85 0
      uwsgi-nginx/tests/test_02_app/test_app_with_installs.py
  50. 87 0
      uwsgi-nginx/tests/test_02_app/test_custom_nginx_app.py
  51. 86 0
      uwsgi-nginx/tests/test_02_app/test_simple_app.py
  52. 63 0
      uwsgi-nginx/tests/utils.py

+ 42 - 0
.github/workflows/uwsgi-nginx.yml

@@ -0,0 +1,42 @@
+name: "uwsgi-nginx docker build"
+
+env:
+  PROJECT: uwsgi-nginx
+
+on:
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    env:
+      ACTIONS_ALLOW_UNSECURE_COMMANDS: true
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Set tag
+        id: tag
+        run: |
+          TAG=$(cat ${{ env.PROJECT }}/docker-images/Dockerfile | awk 'NR==4 {print $3}')
+          echo "::set-env name=TAG::$TAG"
+      - name: Docker Hub login
+        env:
+          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
+        run: |
+          echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
+      - name: Set up Docker Buildx
+        id: buildx
+        uses: crazy-max/ghaction-docker-buildx@v1
+        with:
+          buildx-version: latest
+      - name: Build Dockerfile
+        env:
+          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+        run: |
+          docker buildx build \
+          --platform=linux/amd64,linux/arm64 \
+          --output "type=image,push=true" \
+          --file ${{ env.PROJECT }}/docker-images/Dockerfile ./${{ env.PROJECT }}/docker-images \
+          --tag $(echo "${DOCKER_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ env.PROJECT }}:latest \
+          --tag $(echo "${DOCKER_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ env.PROJECT }}:${TAG}

+ 112 - 0
uwsgi-nginx/.gitignore

@@ -0,0 +1,112 @@
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+.mypy_cache
+.vscode
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
+
+*.iml
+
+## Directory-based project format:
+.idea/
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+# .idea/workspace.xml
+# .idea/tasks.xml
+# .idea/dictionaries
+
+# Sensitive or high-churn files:
+# .idea/dataSources.ids
+# .idea/dataSources.xml
+# .idea/sqlDataSources.xml
+# .idea/dynamic.xml
+# .idea/uiDesigner.xml
+
+# Gradle:
+# .idea/gradle.xml
+# .idea/libraries
+
+# Mongo Explorer plugin:
+# .idea/mongoSettings.xml
+
+## File-based project format:
+*.ipr
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+
+# Custom
+Pipfile.lock
+poetry.lock

+ 201 - 0
uwsgi-nginx/LICENSE.txt

@@ -0,0 +1,201 @@
+                                Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 395 - 0
uwsgi-nginx/README.md

@@ -0,0 +1,395 @@
+[![Test](https://github.com/tiangolo/uwsgi-nginx-docker/workflows/Test/badge.svg)](https://github.com/tiangolo/uwsgi-nginx-docker/actions?query=workflow%3ATest) [![Deploy](https://github.com/tiangolo/uwsgi-nginx-docker/workflows/Deploy/badge.svg)](https://github.com/tiangolo/uwsgi-nginx-docker/actions?query=workflow%3ADeploy)
+
+## Supported tags and respective `Dockerfile` links
+
+* [`python3.8`, `latest` _(Dockerfile)_](https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/docker-images/python3.8.dockerfile)
+* [`python3.8-alpine` _(Dockerfile)_](https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/docker-images/python3.8-alpine.dockerfile)
+* [`python3.7`, _(Dockerfile)_](https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/docker-images/python3.7.dockerfile)
+* [`python3.6` _(Dockerfile)_](https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/docker-images/python3.6.dockerfile)
+* [`python2.7` _(Dockerfile)_](https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/docker-images/python2.7.dockerfile)
+
+**Note**: Note: There are [tags for each build date](https://hub.docker.com/r/tiangolo/uwsgi-nginx/tags). If you need to "pin" the Docker image version you use, you can select one of those tags. E.g. `tiangolo/uwsgi-nginx:python3.7-2019-09-28`.
+
+# uwsgi-nginx
+
+**Docker** image with **uWSGI** and **Nginx** for web applications in **Python 3.6** and above, and **Python 2.7** (as **Flask**) in a single container. Optionally with Alpine Linux.
+
+## Description
+
+This [**Docker**](https://www.docker.com/) image allows you to create [**Python**](https://www.python.org/) web applications that run with [**uWSGI**](https://uwsgi-docs.readthedocs.org/en/latest/) and [**Nginx**](http://nginx.org/en/) in a single container.
+
+The combination of uWSGI with Nginx is a [common way to deploy Python web applications like Flask and Django](http://flask.pocoo.org/docs/1.0/deploying/uwsgi/). It is widely used in the industry and would give you decent performance. (*)
+
+There is also an Alpine version. If you want it, check the tags from above.
+
+This image was created to be the base image for [**tiangolo/uwsgi-nginx-flask**](https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask/) but could be used as the base image for any other (WSGI-based) Python web application, like Django.
+
+### * Note on performance and features
+
+If you are starting a new project, you might benefit from a newer and faster framework based on ASGI instead of WSGI (Flask and Django are WSGI-based).
+
+You could use an ASGI framework like:
+
+* [**FastAPI**](https://github.com/tiangolo/fastapi) (which is based on Starlette) with this Docker image: [**tiangolo/uvicorn-gunicorn-fastapi**](https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker).
+* [**Starlette**](https://github.com/encode/starlette) directly, with this Docker image: [**tiangolo/uvicorn-gunicorn-starlette**](https://github.com/tiangolo/uvicorn-gunicorn-starlette-docker).
+* Or any other ASGI framework with this Docker image: [**tiangolo/uvicorn-gunicorn**](https://github.com/tiangolo/uvicorn-gunicorn-docker).
+
+FastAPI, or Starlette, would give you about 800% (8x) the performance achievable with this image (**tiangolo/uwsgi-nginx**). [You can see the third-party benchmarks here](https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=query&l=z8kflr-v&a=2).
+
+Also, if you want to use new technologies like WebSockets it would be easier (and *possible*) with a newer framework based on ASGI, like FastAPI or Starlette. As the standard ASGI was designed to be able to handle asynchronous code like the one needed for WebSockets.
+
+#### If you need a WSGI-based application (like Flask or Django)
+
+If you need to use an older WSGI-based framework like Flask or Django (instead of something based on ASGI) and you need to have the best performance possible, you can use the alternative image: [**tiangolo/meinheld-gunicorn**](https://github.com/tiangolo/meinheld-gunicorn-docker).
+
+**tiangolo/meinheld-gunicorn** will give you about 400% (4x) the performance of this image.
+
+---
+
+**GitHub repo**: [https://github.com/tiangolo/uwsgi-nginx-docker](https://github.com/tiangolo/uwsgi-nginx-docker)
+
+**Docker Hub image**: [https://hub.docker.com/r/tiangolo/uwsgi-nginx/](https://hub.docker.com/r/tiangolo/uwsgi-nginx/)
+
+## How to use
+
+* You shouldn't have to clone the GitHub repo. You should use it as a base image for other images, using this in your `Dockerfile`:
+
+```Dockerfile
+FROM tiangolo/uwsgi-nginx:python3.8
+
+# Your Dockerfile code...
+```
+
+* But, if you need Python 2.7 that line would have to be `FROM tiangolo/uwsgi-nginx:python2.7`.
+
+* By default it will try to find a uWSGI config file in `/app/uwsgi.ini`.
+
+* That `uwsgi.ini` file will make it try to run a Python file in `/app/main.py`.
+
+If you are building a **Flask** web application you should use instead [**tiangolo/uwsgi-nginx-flask**](https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask/).
+
+## Advanced usage
+
+### Custom app directory
+
+If you need to use a directory for your app different than `/app`, you can override the uWSGI config file path with an environment variable `UWSGI_INI`, and put your custom `uwsgi.ini` file there.
+
+For example, if you needed to have your application directory in `/application` instead of `/app`, your `Dockerfile` would look like:
+
+```Dockerfile
+FROM tiangolo/uwsgi-nginx:python3.8
+
+ENV UWSGI_INI /application/uwsgi.ini
+
+COPY ./application /application
+WORKDIR /appapplication
+```
+
+And your `uwsgi.ini` file in `./application/uwsgi.ini` would contain:
+
+```ini
+[uwsgi]
+wsgi-file=/application/main.py
+```
+
+**Note**: it's important to include the `WORKDIR` option, otherwise uWSGI will start the application in `/app`.
+
+### Custom uWSGI process number
+
+By default, the image starts with 2 uWSGI processes running. When the server is experiencing a high load, it creates up to 16 uWSGI processes to handle it on demand.
+
+If you need to configure these numbers you can use environment variables.
+
+The starting number of uWSGI processes is controlled by the variable `UWSGI_CHEAPER`, by default set to `2`.
+
+The maximum number of uWSGI processes is controlled by the variable `UWSGI_PROCESSES`, by default set to `16`.
+
+Have in mind that `UWSGI_CHEAPER` must be lower than `UWSGI_PROCESSES`.
+
+So, if, for example, you need to start with 4 processes and grow to a maximum of 64, your `Dockerfile` could look like:
+
+```Dockerfile
+FROM tiangolo/uwsgi-nginx:python3.8
+
+ENV UWSGI_CHEAPER 4
+ENV UWSGI_PROCESSES 64
+
+COPY ./app /app
+```
+
+### Custom max upload size
+
+In this image, Nginx is configured to allow unlimited upload file sizes. This is done because by default a simple Python server would allow that, so that's the simplest behavior a developer would expect.
+
+If you need to restrict the maximum upload size in Nginx, you can add an environment variable `NGINX_MAX_UPLOAD` and assign a value corresponding to the [standard Nginx config `client_max_body_size`](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
+
+For example, if you wanted to set the maximum upload file size to 1 MB (the default in a normal Nginx installation), you would need to set the `NGINX_MAX_UPLOAD` environment variable to the value `1m`. Then the image would take care of adding the corresponding configuration file (this is done by the `entrypoint.sh`).
+
+So, your `Dockerfile` would look something like:
+
+```Dockerfile
+FROM tiangolo/uwsgi-nginx:python3.8
+
+ENV NGINX_MAX_UPLOAD 1m
+
+COPY ./app /app
+```
+
+### Custom listen port
+
+By default, the container made from this image will listen on port 80.
+
+To change this behavior, set the `LISTEN_PORT` environment variable.
+
+You might also need to create the respective `EXPOSE` Docker instruction.
+
+You can do that in your `Dockerfile`, it would look something like:
+
+```Dockerfile
+FROM tiangolo/uwsgi-nginx:python3.8
+
+ENV LISTEN_PORT 8080
+
+EXPOSE 8080
+
+COPY ./app /app
+```
+
+### Custom `/app/prestart.sh`
+
+If you need to run anything before starting the app, you can add a file `prestart.sh` to the directory `/app`. The image will automatically detect and run it before starting everything.
+
+For example, if you want to add database migrations that are run on startup (e.g. with Alembic, or Django migrations), before starting the app, you could create a `./app/prestart.sh` file in your code directory (that will be copied by your `Dockerfile`) with:
+
+```bash
+#! /usr/bin/env bash
+
+# Let the DB start
+sleep 10;
+# Run migrations
+alembic upgrade head
+```
+
+and it would wait 10 seconds to give the database some time to start and then run that `alembic` command (you could update that to run Django migrations or any other tool you need).
+
+If you need to run a Python script before starting the app, you could make the `/app/prestart.sh` file run your Python script, with something like:
+
+```bash
+#! /usr/bin/env bash
+
+# Run custom Python script before starting
+python /app/my_custom_prestart_script.py
+```
+
+**Note**: The image uses `.` to run the script (as in `. /app/prestart.sh`), so for example, environment variables would persist. If you don't understand the previous sentence, you probably don't need it.
+
+### Custom Nginx processes number
+
+By default, Nginx will start one "worker process".
+
+If you want to set a different number of Nginx worker processes you can use the environment variable `NGINX_WORKER_PROCESSES`.
+
+You can use a specific single number, e.g.:
+
+```Dockerfile
+ENV NGINX_WORKER_PROCESSES 2
+```
+
+or you can set it to the keyword `auto` and it will try to autodetect the number of CPUs available and use that for the number of workers.
+
+For example, using `auto`, your Dockerfile could look like:
+
+```Dockerfile
+FROM tiangolo/uwsgi-nginx:python3.8
+
+ENV NGINX_WORKER_PROCESSES auto
+
+COPY ./app /app
+```
+
+### Custom Nginx maximum connections per worker
+
+By default, Nginx will start with a maximum limit of 1024 connections per worker.
+
+If you want to set a different number you can use the environment variable `NGINX_WORKER_CONNECTIONS`, e.g:
+
+```Dockerfile
+ENV NGINX_WORKER_CONNECTIONS 2048
+```
+
+It cannot exceed the current limit on the maximum number of open files. See how to configure it in the next section.
+
+### Custom Nginx maximum open files
+
+The number connections per Nginx worker cannot exceed the limit on the maximum number of open files.
+
+You can change the limit of open files with the environment variable `NGINX_WORKER_OPEN_FILES`, e.g.:
+
+```Dockerfile
+ENV NGINX_WORKER_OPEN_FILES 2048
+```
+
+### Customizing Nginx additional configurations
+
+If you need to configure Nginx further, you can add `*.conf` files to `/etc/nginx/conf.d/` in your `Dockerfile`.
+
+Just have in mind that the default configurations are created during startup in a file at `/etc/nginx/conf.d/nginx.conf` and `/etc/nginx/conf.d/upload.conf`. So you shouldn't overwrite them. You should name your `*.conf` file with something different than `nginx.conf` or `upload.conf`, for example: `custom.conf`.
+
+**Note**: if you are customizing Nginx, maybe copying configurations from a blog or a StackOverflow answer, have in mind that you probably need to use the [configurations specific to uWSGI](http://nginx.org/en/docs/http/ngx_http_uwsgi_module.html), instead of those for other modules, like for example, `ngx_http_fastcgi_module`.
+
+### Overriding Nginx configuration completely
+
+If you need to configure Nginx even further, completely overriding the defaults, you can add a custom Nginx configuration to `/app/nginx.conf`.
+
+It will be copied to `/etc/nginx/nginx.conf` and used instead of the generated one.
+
+Have in mind that, in that case, this image won't generate any of the Nginx configurations, it will only copy and use your configuration file.
+
+That means that all the environment variables described above that are specific to Nginx won't be used.
+
+It also means that it won't use additional configurations from files in `/etc/nginx/conf.d/*.conf`, unless you explicitly have a section in your custom file `/app/nginx.conf` with:
+
+```conf
+include /etc/nginx/conf.d/*.conf;
+```
+
+If you want to add a custom `/app/nginx.conf` file but don't know where to start from, you can use [the `nginx.conf` used for the tests](https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/tests/test_02_app/custom_nginx_app/app/nginx.conf) and customize it or modify it further.
+
+## Technical details
+
+The combination of uWSGI with Nginx is a [common way to deploy Python web applications](http://flask.pocoo.org/docs/1.0/deploying/uwsgi/).
+
+Roughly:
+
+* **Nginx** is a web server, it takes care of the HTTP connections and also can serve static files directly and more efficiently.
+
+* **uWSGI** is an application server, that's what runs your Python code and it talks with Nginx.
+
+* **Your Python code** has the actual web application, and is run by uWSGI.
+
+This image takes advantage of already slim and optimized existing Docker images (based on Debian as [recommended by Docker](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/)) and implements Docker best practices.
+
+It uses the official Python Docker image, installs uWSGI and on top of that, with the least amount of modifications, adds the official Nginx image (as of 2016-02-14).
+
+And it controls all these processes with Supervisord.
+
+---
+
+There's the rule of thumb that you should have "one process per container".
+
+That helps, for example, isolating an app and its database in different containers.
+
+But if you want to have a "micro-services" approach you may want to [have more than one process in one container](https://valdhaus.co/writings/docker-misconceptions/) if they are all related to the same "service", and you may want to include your Flask code, uWSGI and Nginx in the same container (and maybe run another container with your database).
+
+That's the approach taken in this image.
+
+---
+
+This image has a default sample "Hello World" app in the container's `/app` directory using the example in the [uWSGI documentation](http://uwsgi-docs.readthedocs.org/en/latest/WSGIquickstart.html).
+
+You probably want to override it or delete it in your project.
+
+It is there in case you run this image by itself and not as a base image for your own `Dockerfile`, so that you get a sample app without errors.
+
+## Tests
+
+All the image tags, configurations, environment variables and application options are tested.
+
+## Updates
+
+Updates are announced in the releases.
+
+You can click the "watch" button at the top right and select "Releases only" to receive an email notification when there's a new release.
+
+## Release Notes
+
+### Latest Changes
+
+* 🐛 Fix broken link to TechEmpower benchmarks. PR [#96](https://github.com/tiangolo/uwsgi-nginx-docker/pull/96) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Add GitHub Action latest-changes, update issue-manager. PR [#92](https://github.com/tiangolo/uwsgi-nginx-docker/pull/92) by [@tiangolo](https://github.com/tiangolo).
+* Fix Python 3.8 Alpine environment for installed packages. PR [#84](https://github.com/tiangolo/uwsgi-nginx-docker/pull/84).
+
+### 1.4.0
+
+* Add [GitHub Sponsors](https://github.com/sponsors/tiangolo) button.
+* Add new image for Python 3.8, and new image for Python 3.8 on Alpine. PR [#83](https://github.com/tiangolo/uwsgi-nginx-docker/pull/83).
+* Upgrade Nginx to latest version, `1.17.10`, based on latest Debian, Buster. PR [#82](https://github.com/tiangolo/uwsgi-nginx-docker/pull/82).
+* Remove support for Python 3.5. PR [#81](https://github.com/tiangolo/uwsgi-nginx-docker/pull/81).
+
+### 1.3.0
+
+* This is the last version to support:
+    * Debian Stretch (before upgrading to Buster).
+    * Python 3.5.
+    * Alpine 3.7, 3.8, 3.9 (before upgrading to Alpine 3.11).
+    * Alpine in older versions of Python, 2.7 and 3.6 (Before upgrading to Python 3.8).
+    * If you need any of those, make sure to use a tag for the build date `2020-05-04`.
+* Refactor build set up:
+    * Re-use code and configs.
+    * Migrate to GitHub Actions.
+    * Simplify tests.
+    * PR [#78](https://github.com/tiangolo/uwsgi-nginx-docker/pull/78).
+* Migrate Travis to .com, update badge. PR [#77](https://github.com/tiangolo/uwsgi-nginx-docker/pull/77).
+
+### 1.2.0
+
+* 2019-10-14:
+    * Refactor and simplify test scripts. PR [#66](https://github.com/tiangolo/uwsgi-nginx-docker/pull/66).
+* 2019-09-28:
+    * Refactor build scripts and add image tags for each build date, like `tiangolo/uwsgi-nginx:python3.7-2019-09-28`. PR [#65](https://github.com/tiangolo/uwsgi-nginx-docker/pull/65).
+
+* Upgrade Travis. PR [#60](https://github.com/tiangolo/uwsgi-nginx-docker/pull/60).
+
+### 1.1.0
+
+* Added support for `/app/prestart.sh` script to run arbitrary code before starting the app (for example, Alembic - SQLAlchemy migrations). The [documentation for the `/app/prestart.sh` is in the main README](https://github.com/tiangolo/uwsgi-nginx-docker#custom-appprestartsh). [PR #59](https://github.com/tiangolo/uwsgi-nginx-docker/pull/59).
+
+### 1.0.0
+
+* 2019-05-04:
+    * Add Alpine Linux 3.9. PR [#55](https://github.com/tiangolo/uwsgi-nginx-docker/pull/55) by [evilgoldfish](https://github.com/evilgoldfish).
+    * Build images using Travis matrix to improve development/testing speed. Needed for some recent PRs. [PR #58](https://github.com/tiangolo/uwsgi-nginx-docker/pull/58).
+
+* 2019-02-02:
+    * The Nginx configurations are generated dynamically from the entrypoint, instead of modifying pre-existing files. [PR #50](https://github.com/tiangolo/uwsgi-nginx-docker/pull/50).
+    * Support for a completely custom `/app/nginx.conf` file that overrides the generated one. [PR #51](https://github.com/tiangolo/uwsgi-nginx-docker/pull/51).
+
+* 2018-11-23: New Alpine 3.8 images for Python 2.7, Python 3.6 and Python 3.7 (Python 3.7 temporarily disabled). Thanks to [philippfreyer](https://github.com/philippfreyer) in [PR #45](https://github.com/tiangolo/uwsgi-nginx-docker/pull/45)
+
+* 2018-09-22: New Python 3.7 versions, standard and Alpine based. Thanks to [desaintmartin](https://github.com/desaintmartin) in [this PR](https://github.com/tiangolo/uwsgi-nginx-docker/pull/39).
+
+* 2018-06-22: You can now use `NGINX_WORKER_CONNECTIONS` to set the maximum number of Nginx worker connections and `NGINX_WORKER_OPEN_FILES` to set the maximum number of open files. Thanks to [ronlut](https://github.com/ronlut) in [this PR](https://github.com/tiangolo/uwsgi-nginx-docker/pull/26).
+
+* 2018-06-22: Make uWSGI require an app to run, instead of going in "full dynamic mode" while there was an error. Supervisord doesn't terminate itself but tries to restart uWSGI and shows the errors. Uses `need-app` as suggested by [luckydonald](https://github.com/luckydonald) in [this comment](https://github.com/tiangolo/uwsgi-nginx-flask-docker/issues/3#issuecomment-321991279).
+
+* 2018-06-22: Correctly handled graceful shutdown of uWSGI and Nginx. Thanks to [desaintmartin](https://github.com/desaintmartin) in [this PR](https://github.com/tiangolo/uwsgi-nginx-docker/pull/30).
+
+* 2018-02-04: It's now possible to set the number of Nginx worker processes with the environment variable `NGINX_WORKER_PROCESSES`. Thanks to [naktinis](https://github.com/naktinis) in [this PR](https://github.com/tiangolo/uwsgi-nginx-docker/pull/22).
+
+* 2018-01-14: There are now two Alpine based versions, `python2.7-alpine3.7` and `python3.6-alpine3.7`.
+
+* 2017-12-08: Now you can configure which port the container should listen on, using the environment variable `LISTEN_PORT` thanks to [tmshn](https://github.com/tmshn) in [this PR](https://github.com/tiangolo/uwsgi-nginx-docker/pull/16).
+
+* 2017-08-09: You can set a custom maximum upload file size using an environment variable `NGINX_MAX_UPLOAD`, by default it has a value of `0`, that allows unlimited upload file sizes. This differs from Nginx's default value of 1 MB. It's configured this way because that's the simplest experience a developer that is not expert in Nginx would expect.
+
+* 2017-08-09: Now you can override where to look for the `uwsgi.ini` file, and with that, change the default directory from `/app` to something else, using the envirnoment variable `UWSGI_INI`.
+
+* 2017-08-08: There's a new `latest` tag image, just to show a warning for those still using `latest` for Python 2.7 web applications. As of now, [everyone](https://www.python.org/dev/peps/pep-0373/) [should be](http://flask.pocoo.org/docs/0.12/python3/#python3-support) [using Python 3](https://docs.djangoproject.com/en/1.11/faq/install/#what-python-version-should-i-use-with-django).
+
+* 2017-08-08: Supervisord now terminates uWSGI on `SIGTERM`, so if you run `docker stop` or something similar, it will actually stop everything, instead of waiting for Docker's timeout to kill the container.
+
+* 2017-07-31: There's now an image tag for Python 3.6, based on the official image for Python 3.6 thanks to [jrd](https://github.com/jrd) in [this PR](https://github.com/tiangolo/uwsgi-nginx-docker/pull/6).
+
+* 2016-10-01: Now you can override default `uwsgi.ini` parameters from the file in `/app/uwsgi.ini`.
+
+* 2016-08-16: There's now an image tag for Python 3.5, based on the official image for Python 3.5. So now you can use this image for your projects in Python 2.7 and Python 3.5.
+
+* 2016-08-16: Use dynamic a number of worker processes for uWSGI, from 2 to 16 depending on load. This should work for most cases. This helps especially when there are some responses that are slow and take some time to be generated, this change allows all the other responses to keep fast (in a new process) without having to wait for the first (slow) one to finish.
+
+* Also, it now uses a base `uwsgi.ini` file under `/etc/uwsgi/` with most of the general configurations, so, the `uwsgi.ini` inside `/app` (the one you could need to modify) is now a lot simpler.
+
+* 2016-04-05: Nginx and uWSGI logs are now redirected to stdout, allowing to use `docker logs`.
+
+## License
+
+This project is licensed under the terms of the Apache license.

+ 41 - 0
uwsgi-nginx/backup.travis.yml

@@ -0,0 +1,41 @@
+dist: xenial
+
+language: python
+
+python:
+  - "3.7"
+
+install:
+  - pip install docker pytest
+
+services:
+  - docker
+
+env:
+  - NAME='python2.7' BUILD_PATH='python2.7' TEST_STR1='Hello World from a default Nginx uWSGI Python 2.7 app in a Docker container (default)' TEST_STR2='Hello World from Nginx uWSGI Python 2.7 app in a Docker container' RUN_TESTS='1'
+  - NAME='python2.7-alpine3.7' BUILD_PATH='python2.7-alpine3.7' TEST_STR1='Hello World from a default Nginx uWSGI Python 2.7 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 2.7 app in a Docker container' RUN_TESTS='1'
+  - NAME='python2.7-alpine3.8' BUILD_PATH='python2.7-alpine3.8' TEST_STR1='Hello World from a default Nginx uWSGI Python 2.7 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 2.7 app in a Docker container' RUN_TESTS='1'
+  - NAME='python2.7-alpine3.9' BUILD_PATH='python2.7-alpine3.9' TEST_STR1='Hello World from a default Nginx uWSGI Python 2.7 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 2.7 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.5' BUILD_PATH='python3.5' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.5 app in a Docker container (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.5 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.6' BUILD_PATH='python3.6' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.6 app in a Docker container (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.6 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.6-alpine3.7' BUILD_PATH='python3.6-alpine3.7' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.6 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.6 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.6-alpine3.8' BUILD_PATH='python3.6-alpine3.8' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.6 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.6 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.6-alpine3.9' BUILD_PATH='python3.6-alpine3.9' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.6 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.6 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.7' BUILD_PATH='python3.7' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.7 app in a Docker container (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.7 app in a Docker container' RUN_TESTS='1'
+  - NAME='latest' BUILD_PATH='python3.7' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.7 app in a Docker container (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.7 app in a Docker container' RUN_TESTS='1'
+  - NAME='python3.7-alpine3.7' BUILD_PATH='python3.7-alpine3.7' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.7 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.7 app in a Docker container' RUN_TESTS=''
+  - NAME='python3.7-alpine3.8' BUILD_PATH='python3.7-alpine3.8' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.7 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.7 app in a Docker container' RUN_TESTS=''
+  - NAME='python3.7-alpine3.9' BUILD_PATH='python3.7-alpine3.9' TEST_STR1='Hello World from a default Nginx uWSGI Python 3.7 app in a Docker container in Alpine (default)' TEST_STR2='Hello World from Nginx uWSGI Python 3.7 app in a Docker container' RUN_TESTS=''
+
+script: bash scripts/test.sh
+
+jobs:
+    include:
+      - script: bash scripts/test.sh
+      - stage: deploy
+        script: skip
+        deploy:
+          provider: script
+          script: bash scripts/build-push-all.sh
+          on:
+            branch: master

+ 67 - 0
uwsgi-nginx/docker-images/Dockerfile

@@ -0,0 +1,67 @@
+FROM python:3.7-buster
+
+LABEL maintainer="Sebastian Ramirez <[email protected]>"
+ENV VERSION python3.7
+COPY install-nginx-debian.sh /
+
+RUN bash /install-nginx-debian.sh
+
+EXPOSE 80
+
+# Expose 443, in case of LTS / HTTPS
+EXPOSE 443
+
+# Install uWSGI
+RUN pip install uwsgi
+
+# Remove default configuration from Nginx
+RUN rm /etc/nginx/conf.d/default.conf
+# Copy the base uWSGI ini file to enable default dynamic uwsgi process number
+COPY uwsgi.ini /etc/uwsgi/
+
+# Install Supervisord
+RUN apt-get update && apt-get install -y supervisor \
+&& rm -rf /var/lib/apt/lists/*
+# Custom Supervisord config
+COPY supervisord-debian.conf /etc/supervisor/conf.d/supervisord.conf
+
+# Which uWSGI .ini file should be used, to make it customizable
+ENV UWSGI_INI /app/uwsgi.ini
+
+# By default, run 2 processes
+ENV UWSGI_CHEAPER 2
+
+# By default, when on demand, run up to 16 processes
+ENV UWSGI_PROCESSES 16
+
+# By default, allow unlimited file sizes, modify it to limit the file sizes
+# To have a maximum of 1 MB (Nginx's default) change the line to:
+# ENV NGINX_MAX_UPLOAD 1m
+ENV NGINX_MAX_UPLOAD 0
+
+# By default, Nginx will run a single worker process, setting it to auto
+# will create a worker for each CPU core
+ENV NGINX_WORKER_PROCESSES 1
+
+# By default, Nginx listens on port 80.
+# To modify this, change LISTEN_PORT environment variable.
+# (in a Dockerfile or with an option for `docker run`)
+ENV LISTEN_PORT 80
+
+# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app
+COPY start.sh /start.sh
+RUN chmod +x /start.sh
+
+# Copy the entrypoint that will generate Nginx additional configs
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+# Add demo app
+COPY ./app /app
+WORKDIR /app
+
+# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations)
+# And then will start Supervisor, which in turn will start Nginx and uWSGI
+CMD ["/start.sh"]

+ 10 - 0
uwsgi-nginx/docker-images/app/main.py

@@ -0,0 +1,10 @@
+import sys
+
+
+def application(env, start_response):
+    version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
+    start_response("200 OK", [("Content-Type", "text/plain")])
+    message = "Hello World from a default Nginx uWSGI Python {} app in a Docker container (default)".format(
+        version
+    )
+    return [message.encode("utf-8")]

+ 12 - 0
uwsgi-nginx/docker-images/app/prestart.sh

@@ -0,0 +1,12 @@
+#! /usr/bin/env sh
+
+echo "Running inside /app/prestart.sh, you could add migrations to this file, e.g.:"
+
+echo "
+#! /usr/bin/env bash
+
+# Let the DB start
+sleep 10;
+# Run migrations
+alembic upgrade head
+"

+ 2 - 0
uwsgi-nginx/docker-images/app/uwsgi.ini

@@ -0,0 +1,2 @@
+[uwsgi]
+wsgi-file=/app/main.py

+ 70 - 0
uwsgi-nginx/docker-images/entrypoint.sh

@@ -0,0 +1,70 @@
+#!/usr/bin/env sh
+set -e
+
+# Get the maximum upload file size for Nginx, default to 0: unlimited
+USE_NGINX_MAX_UPLOAD=${NGINX_MAX_UPLOAD:-0}
+
+# Get the number of workers for Nginx, default to 1
+USE_NGINX_WORKER_PROCESSES=${NGINX_WORKER_PROCESSES:-1}
+
+# Set the max number of connections per worker for Nginx, if requested 
+# Cannot exceed worker_rlimit_nofile, see NGINX_WORKER_OPEN_FILES below
+NGINX_WORKER_CONNECTIONS=${NGINX_WORKER_CONNECTIONS:-1024}
+
+# Get the listen port for Nginx, default to 80
+USE_LISTEN_PORT=${LISTEN_PORT:-80}
+
+if [ -f /app/nginx.conf ]; then
+    cp /app/nginx.conf /etc/nginx/nginx.conf
+else
+    content='user  nginx;\n'
+    # Set the number of worker processes in Nginx
+    content=$content"worker_processes ${USE_NGINX_WORKER_PROCESSES};\n"
+    content=$content'error_log  /var/log/nginx/error.log warn;\n'
+    content=$content'pid        /var/run/nginx.pid;\n'
+    content=$content'events {\n'
+    content=$content"    worker_connections ${NGINX_WORKER_CONNECTIONS};\n"
+    content=$content'}\n'
+    content=$content'http {\n'
+    content=$content'    include       /etc/nginx/mime.types;\n'
+    content=$content'    default_type  application/octet-stream;\n'
+    content=$content'    log_format  main  '"'\$remote_addr - \$remote_user [\$time_local] \"\$request\" '\n"
+    content=$content'                      '"'\$status \$body_bytes_sent \"\$http_referer\" '\n"
+    content=$content'                      '"'\"\$http_user_agent\" \"\$http_x_forwarded_for\"';\n"
+    content=$content'    access_log  /var/log/nginx/access.log  main;\n'
+    content=$content'    sendfile        on;\n'
+    content=$content'    keepalive_timeout  65;\n'
+    content=$content'    include /etc/nginx/conf.d/*.conf;\n'
+    content=$content'}\n'
+    content=$content'daemon off;\n'
+    # Set the max number of open file descriptors for Nginx workers, if requested
+    if [ -n "${NGINX_WORKER_OPEN_FILES}" ] ; then
+        content=$content"worker_rlimit_nofile ${NGINX_WORKER_OPEN_FILES};\n"
+    fi
+    # Save generated /etc/nginx/nginx.conf
+    printf "$content" > /etc/nginx/nginx.conf
+
+    content_server='server {\n'
+    content_server=$content_server"    listen ${USE_LISTEN_PORT};\n"
+    content_server=$content_server'    location / {\n'
+    content_server=$content_server'        include uwsgi_params;\n'
+    content_server=$content_server'        uwsgi_pass unix:///tmp/uwsgi.sock;\n'
+    content_server=$content_server'    }\n'
+    content_server=$content_server'}\n'
+    # Save generated server /etc/nginx/conf.d/nginx.conf
+    printf "$content_server" > /etc/nginx/conf.d/nginx.conf
+
+    # Generate Nginx config for maximum upload file size
+    printf "client_max_body_size $USE_NGINX_MAX_UPLOAD;\n" > /etc/nginx/conf.d/upload.conf
+
+    # Remove default Nginx config from Alpine
+    printf "" > /etc/nginx/conf.d/default.conf
+fi
+
+# For Alpine:
+# Explicitly add installed Python packages and uWSGI Python packages to PYTHONPATH
+# Otherwise uWSGI can't import Flask
+if [ -n "$ALPINEPYTHON" ] ; then
+    export PYTHONPATH=$PYTHONPATH:/usr/local/lib/$ALPINEPYTHON/site-packages:/usr/lib/$ALPINEPYTHON/site-packages
+fi
+exec "$@"

+ 95 - 0
uwsgi-nginx/docker-images/install-nginx-alpine.sh

@@ -0,0 +1,95 @@
+#! /usr/bin/env sh
+
+# From official Nginx Docker image, as a script to re-use it, removing internal comments
+
+# Standard set up Nginx Alpine
+# https://github.com/nginxinc/docker-nginx/blob/594ce7a8bc26c85af88495ac94d5cd0096b306f7/mainline/alpine/Dockerfile
+
+export NGINX_VERSION=1.17.10
+export NJS_VERSION=0.3.9
+export PKG_RELEASE=1
+
+set -x \
+    && apkArch="$(cat /etc/apk/arch)" \
+    && nginxPackages=" \
+        nginx=${NGINX_VERSION}-r${PKG_RELEASE} \
+        nginx-module-xslt=${NGINX_VERSION}-r${PKG_RELEASE} \
+        nginx-module-geoip=${NGINX_VERSION}-r${PKG_RELEASE} \
+        nginx-module-image-filter=${NGINX_VERSION}-r${PKG_RELEASE} \
+        nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-r${PKG_RELEASE} \
+    " \
+    && case "$apkArch" in \
+        x86_64) \
+            set -x \
+            && KEY_SHA512="e7fa8303923d9b95db37a77ad46c68fd4755ff935d0a534d26eba83de193c76166c68bfe7f65471bf8881004ef4aa6df3e34689c305662750c0172fca5d8552a *stdin" \
+            && apk add --no-cache --virtual .cert-deps \
+                openssl \
+            && wget -O /tmp/nginx_signing.rsa.pub https://nginx.org/keys/nginx_signing.rsa.pub \
+            && if [ "$(openssl rsa -pubin -in /tmp/nginx_signing.rsa.pub -text -noout | openssl sha512 -r)" = "$KEY_SHA512" ]; then \
+                echo "key verification succeeded!"; \
+                mv /tmp/nginx_signing.rsa.pub /etc/apk/keys/; \
+            else \
+                echo "key verification failed!"; \
+                exit 1; \
+            fi \
+            && apk del .cert-deps \
+            && apk add -X "https://nginx.org/packages/mainline/alpine/v$(egrep -o '^[0-9]+\.[0-9]+' /etc/alpine-release)/main" --no-cache $nginxPackages \
+            ;; \
+        *) \
+            set -x \
+            && tempDir="$(mktemp -d)" \
+            && chown nobody:nobody $tempDir \
+            && apk add --no-cache --virtual .build-deps \
+                gcc \
+                libc-dev \
+                make \
+                openssl-dev \
+                pcre-dev \
+                zlib-dev \
+                linux-headers \
+                libxslt-dev \
+                gd-dev \
+                geoip-dev \
+                perl-dev \
+                libedit-dev \
+                mercurial \
+                bash \
+                alpine-sdk \
+                findutils \
+            && su nobody -s /bin/sh -c " \
+                export HOME=${tempDir} \
+                && cd ${tempDir} \
+                && hg clone https://hg.nginx.org/pkg-oss \
+                && cd pkg-oss \
+                && hg up ${NGINX_VERSION}-${PKG_RELEASE} \
+                && cd alpine \
+                && make all \
+                && apk index -o ${tempDir}/packages/alpine/${apkArch}/APKINDEX.tar.gz ${tempDir}/packages/alpine/${apkArch}/*.apk \
+                && abuild-sign -k ${tempDir}/.abuild/abuild-key.rsa ${tempDir}/packages/alpine/${apkArch}/APKINDEX.tar.gz \
+                " \
+            && cp ${tempDir}/.abuild/abuild-key.rsa.pub /etc/apk/keys/ \
+            && apk del .build-deps \
+            && apk add -X ${tempDir}/packages/alpine/ --no-cache $nginxPackages \
+            ;; \
+    esac \
+    && if [ -n "$tempDir" ]; then rm -rf "$tempDir"; fi \
+    && if [ -n "/etc/apk/keys/abuild-key.rsa.pub" ]; then rm -f /etc/apk/keys/abuild-key.rsa.pub; fi \
+    && if [ -n "/etc/apk/keys/nginx_signing.rsa.pub" ]; then rm -f /etc/apk/keys/nginx_signing.rsa.pub; fi \
+    && apk add --no-cache --virtual .gettext gettext \
+    && mv /usr/bin/envsubst /tmp/ \
+    \
+    && runDeps="$( \
+        scanelf --needed --nobanner /tmp/envsubst \
+            | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
+            | sort -u \
+            | xargs -r apk info --installed \
+            | sort -u \
+    )" \
+    && apk add --no-cache $runDeps \
+    && apk del .gettext \
+    && mv /tmp/envsubst /usr/local/bin/ \
+    && apk add --no-cache tzdata \
+    && ln -sf /dev/stdout /var/log/nginx/access.log \
+    && ln -sf /dev/stderr /var/log/nginx/error.log
+
+# Standard set up Nginx finished

+ 81 - 0
uwsgi-nginx/docker-images/install-nginx-debian.sh

@@ -0,0 +1,81 @@
+#! /usr/bin/env bash
+
+# From official Nginx Docker image, as a script to re-use it, removing internal comments
+# Ref: https://github.com/nginxinc/docker-nginx/blob/594ce7a8bc26c85af88495ac94d5cd0096b306f7/mainline/buster/Dockerfile
+
+# Standard set up Nginx
+export NGINX_VERSION=1.17.10
+export NJS_VERSION=0.3.9
+export PKG_RELEASE=1~buster
+
+set -x \
+    && apt-get update \
+    && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \
+    && \
+    NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
+    found=''; \
+    for server in \
+        ha.pool.sks-keyservers.net \
+        hkp://keyserver.ubuntu.com:80 \
+        hkp://p80.pool.sks-keyservers.net:80 \
+        pgp.mit.edu \
+    ; do \
+        echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
+        apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
+    done; \
+    test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
+    apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \
+    && dpkgArch="$(dpkg --print-architecture)" \
+    && nginxPackages=" \
+        nginx=${NGINX_VERSION}-${PKG_RELEASE} \
+        nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
+        nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
+        nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
+        nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-${PKG_RELEASE} \
+    " \
+    && case "$dpkgArch" in \
+        amd64|i386) \
+            echo "deb https://nginx.org/packages/mainline/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \
+            && apt-get update \
+            ;; \
+        *) \
+            echo "deb-src https://nginx.org/packages/mainline/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \
+            \
+            && tempDir="$(mktemp -d)" \
+            && chmod 777 "$tempDir" \
+            \
+            && savedAptMark="$(apt-mark showmanual)" \
+            \
+            && apt-get update \
+            && apt-get build-dep -y $nginxPackages \
+            && ( \
+                cd "$tempDir" \
+                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
+                    apt-get source --compile $nginxPackages \
+            ) \
+            \
+            && apt-mark showmanual | xargs apt-mark auto > /dev/null \
+            && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \
+            \
+            && ls -lAFh "$tempDir" \
+            && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \
+            && grep '^Package: ' "$tempDir/Packages" \
+            && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \
+            && apt-get -o Acquire::GzipIndexes=false update \
+            ;; \
+    esac \
+    \
+    && apt-get install --no-install-recommends --no-install-suggests -y \
+                        $nginxPackages \
+                        gettext-base \
+    && apt-get remove --purge --auto-remove -y ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
+    \
+    && if [ -n "$tempDir" ]; then \
+        apt-get purge -y --auto-remove \
+        && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
+    fi
+
+# forward request and error logs to docker log collector
+ln -sf /dev/stdout /var/log/nginx/access.log \
+    && ln -sf /dev/stderr /var/log/nginx/error.log
+# Standard set up Nginx finished

+ 67 - 0
uwsgi-nginx/docker-images/python2.7.dockerfile

@@ -0,0 +1,67 @@
+FROM python:2.7-buster
+
+LABEL maintainer="Sebastian Ramirez <[email protected]>"
+
+COPY install-nginx-debian.sh /
+
+RUN bash /install-nginx-debian.sh
+
+EXPOSE 80
+
+# Expose 443, in case of LTS / HTTPS
+EXPOSE 443
+
+# Install uWSGI
+RUN pip install uwsgi
+
+# Remove default configuration from Nginx
+RUN rm /etc/nginx/conf.d/default.conf
+# Copy the base uWSGI ini file to enable default dynamic uwsgi process number
+COPY uwsgi.ini /etc/uwsgi/
+
+# Install Supervisord
+RUN apt-get update && apt-get install -y supervisor \
+&& rm -rf /var/lib/apt/lists/*
+# Custom Supervisord config
+COPY supervisord-debian.conf /etc/supervisor/conf.d/supervisord.conf
+
+# Which uWSGI .ini file should be used, to make it customizable
+ENV UWSGI_INI /app/uwsgi.ini
+
+# By default, run 2 processes
+ENV UWSGI_CHEAPER 2
+
+# By default, when on demand, run up to 16 processes
+ENV UWSGI_PROCESSES 16
+
+# By default, allow unlimited file sizes, modify it to limit the file sizes
+# To have a maximum of 1 MB (Nginx's default) change the line to:
+# ENV NGINX_MAX_UPLOAD 1m
+ENV NGINX_MAX_UPLOAD 0
+
+# By default, Nginx will run a single worker process, setting it to auto
+# will create a worker for each CPU core
+ENV NGINX_WORKER_PROCESSES 1
+
+# By default, Nginx listens on port 80.
+# To modify this, change LISTEN_PORT environment variable.
+# (in a Dockerfile or with an option for `docker run`)
+ENV LISTEN_PORT 80
+
+# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app
+COPY start.sh /start.sh
+RUN chmod +x /start.sh
+
+# Copy the entrypoint that will generate Nginx additional configs
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+# Add demo app
+COPY ./app /app
+WORKDIR /app
+
+# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations)
+# And then will start Supervisor, which in turn will start Nginx and uWSGI
+CMD ["/start.sh"]

+ 67 - 0
uwsgi-nginx/docker-images/python3.6.dockerfile

@@ -0,0 +1,67 @@
+FROM python:3.6-buster
+
+LABEL maintainer="Sebastian Ramirez <[email protected]>"
+
+COPY install-nginx-debian.sh /
+
+RUN bash /install-nginx-debian.sh
+
+EXPOSE 80
+
+# Expose 443, in case of LTS / HTTPS
+EXPOSE 443
+
+# Install uWSGI
+RUN pip install uwsgi
+
+# Remove default configuration from Nginx
+RUN rm /etc/nginx/conf.d/default.conf
+# Copy the base uWSGI ini file to enable default dynamic uwsgi process number
+COPY uwsgi.ini /etc/uwsgi/
+
+# Install Supervisord
+RUN apt-get update && apt-get install -y supervisor \
+&& rm -rf /var/lib/apt/lists/*
+# Custom Supervisord config
+COPY supervisord-debian.conf /etc/supervisor/conf.d/supervisord.conf
+
+# Which uWSGI .ini file should be used, to make it customizable
+ENV UWSGI_INI /app/uwsgi.ini
+
+# By default, run 2 processes
+ENV UWSGI_CHEAPER 2
+
+# By default, when on demand, run up to 16 processes
+ENV UWSGI_PROCESSES 16
+
+# By default, allow unlimited file sizes, modify it to limit the file sizes
+# To have a maximum of 1 MB (Nginx's default) change the line to:
+# ENV NGINX_MAX_UPLOAD 1m
+ENV NGINX_MAX_UPLOAD 0
+
+# By default, Nginx will run a single worker process, setting it to auto
+# will create a worker for each CPU core
+ENV NGINX_WORKER_PROCESSES 1
+
+# By default, Nginx listens on port 80.
+# To modify this, change LISTEN_PORT environment variable.
+# (in a Dockerfile or with an option for `docker run`)
+ENV LISTEN_PORT 80
+
+# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app
+COPY start.sh /start.sh
+RUN chmod +x /start.sh
+
+# Copy the entrypoint that will generate Nginx additional configs
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+# Add demo app
+COPY ./app /app
+WORKDIR /app
+
+# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations)
+# And then will start Supervisor, which in turn will start Nginx and uWSGI
+CMD ["/start.sh"]

+ 67 - 0
uwsgi-nginx/docker-images/python3.7.dockerfile

@@ -0,0 +1,67 @@
+FROM python:3.7-buster
+
+LABEL maintainer="Sebastian Ramirez <[email protected]>"
+
+COPY install-nginx-debian.sh /
+
+RUN bash /install-nginx-debian.sh
+
+EXPOSE 80
+
+# Expose 443, in case of LTS / HTTPS
+EXPOSE 443
+
+# Install uWSGI
+RUN pip install uwsgi
+
+# Remove default configuration from Nginx
+RUN rm /etc/nginx/conf.d/default.conf
+# Copy the base uWSGI ini file to enable default dynamic uwsgi process number
+COPY uwsgi.ini /etc/uwsgi/
+
+# Install Supervisord
+RUN apt-get update && apt-get install -y supervisor \
+&& rm -rf /var/lib/apt/lists/*
+# Custom Supervisord config
+COPY supervisord-debian.conf /etc/supervisor/conf.d/supervisord.conf
+
+# Which uWSGI .ini file should be used, to make it customizable
+ENV UWSGI_INI /app/uwsgi.ini
+
+# By default, run 2 processes
+ENV UWSGI_CHEAPER 2
+
+# By default, when on demand, run up to 16 processes
+ENV UWSGI_PROCESSES 16
+
+# By default, allow unlimited file sizes, modify it to limit the file sizes
+# To have a maximum of 1 MB (Nginx's default) change the line to:
+# ENV NGINX_MAX_UPLOAD 1m
+ENV NGINX_MAX_UPLOAD 0
+
+# By default, Nginx will run a single worker process, setting it to auto
+# will create a worker for each CPU core
+ENV NGINX_WORKER_PROCESSES 1
+
+# By default, Nginx listens on port 80.
+# To modify this, change LISTEN_PORT environment variable.
+# (in a Dockerfile or with an option for `docker run`)
+ENV LISTEN_PORT 80
+
+# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app
+COPY start.sh /start.sh
+RUN chmod +x /start.sh
+
+# Copy the entrypoint that will generate Nginx additional configs
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+# Add demo app
+COPY ./app /app
+WORKDIR /app
+
+# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations)
+# And then will start Supervisor, which in turn will start Nginx and uWSGI
+CMD ["/start.sh"]

+ 72 - 0
uwsgi-nginx/docker-images/python3.8-alpine.dockerfile

@@ -0,0 +1,72 @@
+FROM python:3.8-alpine3.11
+
+LABEL maintainer="Sebastian Ramirez <[email protected]>"
+
+COPY install-nginx-alpine.sh /
+
+RUN sh /install-nginx-alpine.sh
+
+EXPOSE 80
+
+# # Expose 443, in case of LTS / HTTPS
+EXPOSE 443
+
+# Install uWSGI
+RUN apk add --no-cache uwsgi-python3
+
+# Copy the base uWSGI ini file to enable default dynamic uwsgi process number
+COPY uwsgi.ini /etc/uwsgi/
+
+# Install Supervisord
+RUN apk add --no-cache supervisor
+# Custom Supervisord config
+COPY supervisord-alpine.ini /etc/supervisor.d/supervisord.ini
+
+# uWSGI Python plugin
+# As an env var to re-use the config file
+ENV UWSGI_PLUGIN python3
+
+# Which uWSGI .ini file should be used, to make it customizable
+ENV UWSGI_INI /app/uwsgi.ini
+
+# By default, run 2 processes
+ENV UWSGI_CHEAPER 2
+
+# By default, when on demand, run up to 16 processes
+ENV UWSGI_PROCESSES 16
+
+# By default, allow unlimited file sizes, modify it to limit the file sizes
+# To have a maximum of 1 MB (Nginx's default) change the line to:
+# ENV NGINX_MAX_UPLOAD 1m
+ENV NGINX_MAX_UPLOAD 0
+
+# By default, Nginx will run a single worker process, setting it to auto
+# will create a worker for each CPU core
+ENV NGINX_WORKER_PROCESSES 1
+
+# By default, Nginx listens on port 80.
+# To modify this, change LISTEN_PORT environment variable.
+# (in a Dockerfile or with an option for `docker run`)
+ENV LISTEN_PORT 80
+
+# Used by the entrypoint to explicitly add installed Python packages 
+# and uWSGI Python packages to PYTHONPATH otherwise uWSGI can't import Flask
+ENV ALPINEPYTHON python3.8
+
+# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app
+COPY start.sh /start.sh
+RUN chmod +x /start.sh
+
+# Copy the entrypoint that will generate Nginx additional configs
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["sh", "/entrypoint.sh"]
+
+# Add demo app
+COPY ./app /app
+WORKDIR /app
+
+# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations)
+# And then will start Supervisor, which in turn will start Nginx and uWSGI
+CMD ["/start.sh"]

+ 67 - 0
uwsgi-nginx/docker-images/python3.8.dockerfile

@@ -0,0 +1,67 @@
+FROM python:3.8-buster
+
+LABEL maintainer="Sebastian Ramirez <[email protected]>"
+
+COPY install-nginx-debian.sh /
+
+RUN bash /install-nginx-debian.sh
+
+EXPOSE 80
+
+# Expose 443, in case of LTS / HTTPS
+EXPOSE 443
+
+# Install uWSGI
+RUN pip install uwsgi
+
+# Remove default configuration from Nginx
+RUN rm /etc/nginx/conf.d/default.conf
+# Copy the base uWSGI ini file to enable default dynamic uwsgi process number
+COPY uwsgi.ini /etc/uwsgi/
+
+# Install Supervisord
+RUN apt-get update && apt-get install -y supervisor \
+&& rm -rf /var/lib/apt/lists/*
+# Custom Supervisord config
+COPY supervisord-debian.conf /etc/supervisor/conf.d/supervisord.conf
+
+# Which uWSGI .ini file should be used, to make it customizable
+ENV UWSGI_INI /app/uwsgi.ini
+
+# By default, run 2 processes
+ENV UWSGI_CHEAPER 2
+
+# By default, when on demand, run up to 16 processes
+ENV UWSGI_PROCESSES 16
+
+# By default, allow unlimited file sizes, modify it to limit the file sizes
+# To have a maximum of 1 MB (Nginx's default) change the line to:
+# ENV NGINX_MAX_UPLOAD 1m
+ENV NGINX_MAX_UPLOAD 0
+
+# By default, Nginx will run a single worker process, setting it to auto
+# will create a worker for each CPU core
+ENV NGINX_WORKER_PROCESSES 1
+
+# By default, Nginx listens on port 80.
+# To modify this, change LISTEN_PORT environment variable.
+# (in a Dockerfile or with an option for `docker run`)
+ENV LISTEN_PORT 80
+
+# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app
+COPY start.sh /start.sh
+RUN chmod +x /start.sh
+
+# Copy the entrypoint that will generate Nginx additional configs
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+# Add demo app
+COPY ./app /app
+WORKDIR /app
+
+# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations)
+# And then will start Supervisor, which in turn will start Nginx and uWSGI
+CMD ["/start.sh"]

+ 15 - 0
uwsgi-nginx/docker-images/start.sh

@@ -0,0 +1,15 @@
+#! /usr/bin/env sh
+set -e
+
+# If there's a prestart.sh script in the /app directory, run it before starting
+PRE_START_PATH=/app/prestart.sh
+echo "Checking for script in $PRE_START_PATH"
+if [ -f $PRE_START_PATH ] ; then
+    echo "Running script $PRE_START_PATH"
+    . $PRE_START_PATH
+else
+    echo "There is no script $PRE_START_PATH"
+fi
+
+# Start Supervisor, with Nginx and uWSGI
+exec /usr/bin/supervisord

+ 18 - 0
uwsgi-nginx/docker-images/supervisord-alpine.ini

@@ -0,0 +1,18 @@
+[supervisord]
+nodaemon=true
+
+[program:uwsgi]
+command=/usr/sbin/uwsgi --ini /etc/uwsgi/uwsgi.ini
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+
+[program:nginx]
+command=/usr/sbin/nginx
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+# Graceful stop, see http://nginx.org/en/docs/control.html
+stopsignal=QUIT

+ 18 - 0
uwsgi-nginx/docker-images/supervisord-debian.conf

@@ -0,0 +1,18 @@
+[supervisord]
+nodaemon=true
+
+[program:uwsgi]
+command=/usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+
+[program:nginx]
+command=/usr/sbin/nginx
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+# Graceful stop, see http://nginx.org/en/docs/control.html
+stopsignal=QUIT

+ 10 - 0
uwsgi-nginx/docker-images/uwsgi.ini

@@ -0,0 +1,10 @@
+[uwsgi]
+socket = /tmp/uwsgi.sock
+chown-socket = nginx:nginx
+chmod-socket = 664
+# Graceful shutdown on SIGTERM, see https://github.com/unbit/uwsgi/issues/849#issuecomment-118869386
+hook-master-start = unix_signal:15 gracefully_kill_them_all
+need-app = true
+die-on-term = true
+# For debugging and testing
+show-config = true

+ 3 - 0
uwsgi-nginx/mypy.ini

@@ -0,0 +1,3 @@
+[mypy]
+disallow_untyped_defs = True
+ignore_missing_imports = True

+ 21 - 0
uwsgi-nginx/pyproject.toml

@@ -0,0 +1,21 @@
+[tool.poetry]
+name = "uwsgi-nginx-docker"
+version = "0.1.0"
+description = "Docker image with uWSGI and Nginx for web applications in Python 3.6 and above and Python 2.7 (as Flask) in a single container. Optionally with Alpine Linux."
+authors = ["Sebastián Ramírez <[email protected]>"]
+license = "MIT"
+
+[tool.poetry.dependencies]
+python = "^3.6"
+docker = "^4.2.0"
+pytest = "^5.4.1"
+
+[tool.poetry.dev-dependencies]
+black = "^19.10b0"
+isort = "^4.3.21"
+autoflake = "^1.3.1"
+mypy = "^0.770"
+
+[build-system]
+requires = ["poetry>=0.12"]
+build-backend = "poetry.masonry.api"

+ 7 - 0
uwsgi-nginx/scripts/build-push-all.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -e
+
+bash scripts/docker-login.sh
+
+BUILD_PUSH=1 python scripts/process_all.py

+ 15 - 0
uwsgi-nginx/scripts/build-push.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -e
+
+use_tag="tiangolo/uwsgi-nginx:$NAME"
+use_dated_tag="${use_tag}-$(date -I)"
+
+bash scripts/build.sh
+
+docker tag "$use_tag" "$use_dated_tag"
+
+bash scripts/docker-login.sh
+
+docker push "$use_tag"
+docker push "$use_dated_tag"

+ 12 - 0
uwsgi-nginx/scripts/build.sh

@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -e
+
+use_tag="tiangolo/uwsgi-nginx:$NAME"
+
+DOCKERFILE="$NAME"
+
+if [ "$NAME" == "latest" ] ; then
+    DOCKERFILE="python3.8"
+fi
+
+docker build -t "$use_tag" --file "./docker-images/${DOCKERFILE}.dockerfile" "./docker-images/"

+ 5 - 0
uwsgi-nginx/scripts/docker-login.sh

@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -e
+
+echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin

+ 6 - 0
uwsgi-nginx/scripts/format-imports.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -x
+
+# Sort imports one per line, so autoflake can remove unused imports
+isort --recursive  --force-single-line-imports --apply ./
+sh ./scripts/format.sh

+ 7 - 0
uwsgi-nginx/scripts/format.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -x
+
+autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place ./ --exclude=__init__.py
+isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply ./
+black ./

+ 8 - 0
uwsgi-nginx/scripts/lint.sh

@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+set -x
+
+mypy ./
+black ./ --check
+isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only

+ 54 - 0
uwsgi-nginx/scripts/process_all.py

@@ -0,0 +1,54 @@
+import os
+import subprocess
+import sys
+
+environments = [
+    {"NAME": "latest", "PYTHON_VERSION": "3.8"},
+    {"NAME": "python3.8", "PYTHON_VERSION": "3.8"},
+    {"NAME": "python3.7", "PYTHON_VERSION": "3.7"},
+    {"NAME": "python3.6", "PYTHON_VERSION": "3.6"},
+    {"NAME": "python2.7", "PYTHON_VERSION": "2.7"},
+    {"NAME": "python3.8-alpine", "PYTHON_VERSION": "3.8"},
+]
+
+start_with = os.environ.get("START_WITH")
+build_push = os.environ.get("BUILD_PUSH")
+
+
+def process_tag(*, env: dict) -> None:
+    use_env = {**os.environ, **env}
+    script = "scripts/test.sh"
+    if build_push:
+        script = "scripts/build-push.sh"
+    return_code = subprocess.call(["bash", script], env=use_env)
+    if return_code != 0:
+        sys.exit(return_code)
+
+
+def print_version_envs() -> None:
+    env_lines = []
+    for env in environments:
+        env_vars = []
+        for key, value in env.items():
+            env_vars.append(f"{key}='{value}'")
+        env_lines.append(" ".join(env_vars))
+    for line in env_lines:
+        print(line)
+
+
+def main() -> None:
+    start_at = 0
+    if start_with:
+        start_at = [
+            i for i, env in enumerate((environments)) if env["NAME"] == start_with
+        ][0]
+    for i, env in enumerate(environments[start_at:]):
+        print(f"Processing tag: {env['NAME']}")
+        process_tag(env=env)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1:
+        print_version_envs()
+    else:
+        main()

+ 4 - 0
uwsgi-nginx/scripts/test-all.sh

@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -e
+
+python scripts/process_all.py

+ 5 - 0
uwsgi-nginx/scripts/test.sh

@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -e
+
+bash scripts/build.sh
+pytest tests "$@"

+ 1 - 0
uwsgi-nginx/tests/.gitignore

@@ -0,0 +1 @@
+Dockerfile

+ 0 - 0
uwsgi-nginx/tests/__init__.py


+ 0 - 0
uwsgi-nginx/tests/test_01_main/__init__.py


+ 78 - 0
uwsgi-nginx/tests/test_01_main/test_defaults.py

@@ -0,0 +1,78 @@
+import os
+import time
+
+import docker
+import requests
+from docker.models.containers import Container
+from requests import Response
+
+from ..utils import (
+    CONTAINER_NAME,
+    get_logs,
+    get_nginx_config,
+    get_response_text1,
+    remove_previous_container,
+)
+
+client = docker.from_env()
+
+
+def verify_container(container: Container, response_text: str) -> None:
+    response: Response = requests.get("http://127.0.0.1:8000")
+    assert response.text == response_text
+    nginx_config = get_nginx_config(container)
+    assert "client_max_body_size 0;" in nginx_config
+    assert "worker_processes 1;" in nginx_config
+    assert "listen 80;" in nginx_config
+    assert "worker_connections 1024;" in nginx_config
+    assert "worker_rlimit_nofile;" not in nginx_config
+    assert "daemon off;" in nginx_config
+    assert "include uwsgi_params;" in nginx_config
+    assert "uwsgi_pass unix:///tmp/uwsgi.sock;" in nginx_config
+    logs = get_logs(container)
+    assert "getting INI configuration from /app/uwsgi.ini" in logs
+    assert "getting INI configuration from /etc/uwsgi/uwsgi.ini" in logs
+    assert "ini = /app/uwsgi.ini" in logs
+    assert "ini = /etc/uwsgi/uwsgi.ini" in logs
+    assert "socket = /tmp/uwsgi.sock" in logs
+    assert "chown-socket = nginx:nginx" in logs
+    assert "chmod-socket = 664" in logs
+    assert "hook-master-start = unix_signal:15 gracefully_kill_them_all" in logs
+    assert "need-app = true" in logs
+    assert "die-on-term = true" in logs
+    assert "show-config = true" in logs
+    assert "wsgi-file = /app/main.py" in logs
+    assert "processes = 16" in logs
+    assert "cheaper = 2" in logs
+    assert "Checking for script in /app/prestart.sh" in logs
+    assert "Running script /app/prestart.sh" in logs
+    assert (
+        "Running inside /app/prestart.sh, you could add migrations to this file" in logs
+    )
+    assert "spawned uWSGI master process" in logs
+    assert "spawned uWSGI worker 1" in logs
+    assert "spawned uWSGI worker 2" in logs
+    assert "spawned uWSGI worker 3" not in logs
+    assert 'running "unix_signal:15 gracefully_kill_them_all" (master-start)' in logs
+    assert "success: nginx entered RUNNING state, process has stayed up for" in logs
+    assert "success: uwsgi entered RUNNING state, process has stayed up for" in logs
+
+
+def test_defaults() -> None:
+    name = os.getenv("NAME")
+    image = f"tiangolo/uwsgi-nginx:{name}"
+    response_text = get_response_text1()
+    sleep_time = int(os.getenv("SLEEP_TIME", 3))
+    remove_previous_container(client)
+    container = client.containers.run(
+        image, name=CONTAINER_NAME, ports={"80": "8000"}, detach=True
+    )
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    # Test that everything works after restarting too
+    container.start()
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    container.remove()

+ 90 - 0
uwsgi-nginx/tests/test_01_main/test_env_vars_1.py

@@ -0,0 +1,90 @@
+import os
+import time
+
+import docker
+import requests
+from docker.models.containers import Container
+
+from ..utils import (
+    CONTAINER_NAME,
+    get_logs,
+    get_nginx_config,
+    get_response_text1,
+    remove_previous_container,
+)
+
+client = docker.from_env()
+
+
+def verify_container(container: Container, response_text: str) -> None:
+    response = requests.get("http://127.0.0.1:8000")
+    assert response.text == response_text
+    nginx_config = get_nginx_config(container)
+    assert "client_max_body_size 1m;" in nginx_config
+    assert "worker_processes 2;" in nginx_config
+    assert "listen 80;" in nginx_config
+    assert "worker_connections 2048;" in nginx_config
+    assert "worker_rlimit_nofile 2048;" in nginx_config
+    assert "daemon off;" in nginx_config
+    assert "listen 80;" in nginx_config
+    assert "include uwsgi_params;" in nginx_config
+    assert "uwsgi_pass unix:///tmp/uwsgi.sock;" in nginx_config
+    logs = get_logs(container)
+    assert "getting INI configuration from /app/uwsgi.ini" in logs
+    assert "getting INI configuration from /etc/uwsgi/uwsgi.ini" in logs
+    assert "ini = /app/uwsgi.ini" in logs
+    assert "ini = /etc/uwsgi/uwsgi.ini" in logs
+    assert "socket = /tmp/uwsgi.sock" in logs
+    assert "chown-socket = nginx:nginx" in logs
+    assert "chmod-socket = 664" in logs
+    assert "hook-master-start = unix_signal:15 gracefully_kill_them_all" in logs
+    assert "need-app = true" in logs
+    assert "die-on-term = true" in logs
+    assert "show-config = true" in logs
+    assert "wsgi-file = /app/main.py" in logs
+    assert "processes = 8" in logs
+    assert "cheaper = 3" in logs
+    assert "Checking for script in /app/prestart.sh" in logs
+    assert "Running script /app/prestart.sh" in logs
+    assert (
+        "Running inside /app/prestart.sh, you could add migrations to this file" in logs
+    )
+    assert "spawned uWSGI master process" in logs
+    assert "spawned uWSGI worker 1" in logs
+    assert "spawned uWSGI worker 2" in logs
+    assert "spawned uWSGI worker 3" in logs
+    assert "spawned uWSGI worker 4" not in logs
+    assert 'running "unix_signal:15 gracefully_kill_them_all" (master-start)' in logs
+    assert "success: nginx entered RUNNING state, process has stayed up for" in logs
+    assert "success: uwsgi entered RUNNING state, process has stayed up for" in logs
+
+
+def test_env_vars_1() -> None:
+    name = os.getenv("NAME")
+    image = f"tiangolo/uwsgi-nginx:{name}"
+    response_text = get_response_text1()
+    sleep_time = int(os.getenv("SLEEP_TIME", 3))
+    remove_previous_container(client)
+    container = client.containers.run(
+        image,
+        name=CONTAINER_NAME,
+        environment={
+            "UWSGI_CHEAPER": 3,
+            "UWSGI_PROCESSES": 8,
+            "NGINX_MAX_UPLOAD": "1m",
+            "NGINX_WORKER_PROCESSES": 2,
+            "NGINX_WORKER_CONNECTIONS": 2048,
+            "NGINX_WORKER_OPEN_FILES": 2048,
+        },
+        ports={"80": "8000"},
+        detach=True,
+    )
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    # Test that everything works after restarting too
+    container.start()
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    container.remove()

+ 0 - 0
uwsgi-nginx/tests/test_02_app/__init__.py


+ 14 - 0
uwsgi-nginx/tests/test_02_app/app_with_installs/app/main.py

@@ -0,0 +1,14 @@
+import sys
+
+from flask import Flask
+
+application = Flask(__name__)
+
+
[email protected]("/")
+def hello():
+    version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
+    message = "Hello World from Nginx uWSGI Python {} app in a Docker container".format(
+        version
+    )
+    return message

+ 0 - 0
uwsgi-nginx/tests/test_02_app/custom_app/application/custom_app/__init__.py


+ 10 - 0
uwsgi-nginx/tests/test_02_app/custom_app/application/custom_app/main.py

@@ -0,0 +1,10 @@
+import sys
+
+
+def application(env, start_response):
+    version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
+    start_response("200 OK", [("Content-Type", "text/plain")])
+    message = "Hello World from Nginx uWSGI Python {} app in a Docker container".format(
+        version
+    )
+    return [message.encode("utf-8")]

+ 2 - 0
uwsgi-nginx/tests/test_02_app/custom_app/application/custom_app/uwsgi.ini

@@ -0,0 +1,2 @@
+[uwsgi]
+wsgi-file=/application/custom_app/main.py

+ 2 - 0
uwsgi-nginx/tests/test_02_app/custom_app/prestart.sh

@@ -0,0 +1,2 @@
+#! /usr/bin/env bash
+echo "custom prestart.sh running"

+ 10 - 0
uwsgi-nginx/tests/test_02_app/custom_nginx_app/app/main.py

@@ -0,0 +1,10 @@
+import sys
+
+
+def application(env, start_response):
+    version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
+    start_response("200 OK", [("Content-Type", "text/plain")])
+    message = "Hello World from Nginx uWSGI Python {} app in a Docker container".format(
+        version
+    )
+    return [message.encode("utf-8")]

+ 25 - 0
uwsgi-nginx/tests/test_02_app/custom_nginx_app/app/nginx.conf

@@ -0,0 +1,25 @@
+user  nginx;
+worker_processes 2;
+error_log  /var/log/nginx/error.log warn;
+pid        /var/run/nginx.pid;
+events {
+    worker_connections 2048;
+}
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+    access_log  /var/log/nginx/access.log  main;
+    sendfile        on;
+    keepalive_timeout  300;
+    server {
+        listen 8080;
+        location / {
+            include uwsgi_params;
+            uwsgi_pass unix:///tmp/uwsgi.sock;
+        }
+    }
+}
+daemon off;

+ 10 - 0
uwsgi-nginx/tests/test_02_app/simple_app/app/main.py

@@ -0,0 +1,10 @@
+import sys
+
+
+def application(env, start_response):
+    version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
+    start_response("200 OK", [("Content-Type", "text/plain")])
+    message = "Hello World from Nginx uWSGI Python {} app in a Docker container".format(
+        version
+    )
+    return [message.encode("utf-8")]

+ 91 - 0
uwsgi-nginx/tests/test_02_app/test_app_and_env_vars.py

@@ -0,0 +1,91 @@
+import os
+import time
+from pathlib import Path
+
+import docker
+import requests
+from docker.models.containers import Container
+
+from ..utils import (
+    CONTAINER_NAME,
+    generate_dockerfile_content_custom_app,
+    get_logs,
+    get_nginx_config,
+    get_response_text2,
+    remove_previous_container,
+)
+
+client = docker.from_env()
+
+
+def verify_container(container: Container, response_text: str) -> None:
+    response = requests.get("http://127.0.0.1:8000")
+    assert response.text == response_text
+    nginx_config = get_nginx_config(container)
+    assert "client_max_body_size 0;" in nginx_config
+    assert "worker_processes 1;" in nginx_config
+    assert "listen 8080;" in nginx_config
+    assert "worker_connections 1024;" in nginx_config
+    assert "worker_rlimit_nofile;" not in nginx_config
+    assert "daemon off;" in nginx_config
+    assert "include uwsgi_params;" in nginx_config
+    assert "uwsgi_pass unix:///tmp/uwsgi.sock;" in nginx_config
+    logs = get_logs(container)
+    assert "getting INI configuration from /application/custom_app/uwsgi.ini" in logs
+    assert "getting INI configuration from /etc/uwsgi/uwsgi.ini" in logs
+    assert "ini = /application/custom_app/uwsgi.ini" in logs
+    assert "ini = /etc/uwsgi/uwsgi.ini" in logs
+    assert "socket = /tmp/uwsgi.sock" in logs
+    assert "chown-socket = nginx:nginx" in logs
+    assert "chmod-socket = 664" in logs
+    assert "hook-master-start = unix_signal:15 gracefully_kill_them_all" in logs
+    assert "need-app = true" in logs
+    assert "die-on-term = true" in logs
+    assert "show-config = true" in logs
+    assert "wsgi-file = /application/custom_app/main.py" in logs
+    assert "processes = 16" in logs
+    assert "cheaper = 2" in logs
+    assert "Checking for script in /app/prestart.sh" in logs
+    assert "Running script /app/prestart.sh" in logs
+    assert "custom prestart.sh running" in logs
+    assert "spawned uWSGI master process" in logs
+    assert "spawned uWSGI worker 1" in logs
+    assert "spawned uWSGI worker 2" in logs
+    assert "spawned uWSGI worker 3" not in logs
+    assert 'running "unix_signal:15 gracefully_kill_them_all" (master-start)' in logs
+    assert "success: nginx entered RUNNING state, process has stayed up for" in logs
+    assert "success: uwsgi entered RUNNING state, process has stayed up for" in logs
+
+
+def test_env_vars_1() -> None:
+    name = os.getenv("NAME", "")
+    dockerfile_content = generate_dockerfile_content_custom_app(name)
+    dockerfile = "Dockerfile"
+    response_text = get_response_text2()
+    sleep_time = int(os.getenv("SLEEP_TIME", 3))
+    remove_previous_container(client)
+    tag = "uwsgi-nginx-testimage"
+    test_path = Path(__file__)
+    path = test_path.parent / "custom_app"
+    dockerfile_path = path / dockerfile
+    dockerfile_path.write_text(dockerfile_content)
+    client.images.build(path=str(path), dockerfile=dockerfile, tag=tag)
+    container = client.containers.run(
+        tag,
+        name=CONTAINER_NAME,
+        environment={
+            "UWSGI_INI": "/application/custom_app/uwsgi.ini",
+            "LISTEN_PORT": "8080",
+        },
+        ports={"8080": "8000"},
+        detach=True,
+    )
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    # Test that everything works after restarting too
+    container.start()
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    container.remove()

+ 85 - 0
uwsgi-nginx/tests/test_02_app/test_app_with_installs.py

@@ -0,0 +1,85 @@
+import os
+import time
+from pathlib import Path
+
+import docker
+import requests
+from docker.models.containers import Container
+
+from ..utils import (
+    CONTAINER_NAME, generate_dockerfile_content_app_with_installs,
+    get_logs,
+    get_nginx_config,
+    get_response_text2,
+    remove_previous_container,
+)
+
+client = docker.from_env()
+
+
+def verify_container(container: Container, response_text: str) -> None:
+    response = requests.get("http://127.0.0.1:8000")
+    assert response.text == response_text
+    nginx_config = get_nginx_config(container)
+    assert "client_max_body_size 0;" in nginx_config
+    assert "worker_processes 1;" in nginx_config
+    assert "listen 80;" in nginx_config
+    assert "worker_connections 1024;" in nginx_config
+    assert "worker_rlimit_nofile;" not in nginx_config
+    assert "daemon off;" in nginx_config
+    assert "include uwsgi_params;" in nginx_config
+    assert "uwsgi_pass unix:///tmp/uwsgi.sock;" in nginx_config
+    logs = get_logs(container)
+    assert "getting INI configuration from /app/uwsgi.ini" in logs
+    assert "getting INI configuration from /etc/uwsgi/uwsgi.ini" in logs
+    assert "ini = /app/uwsgi.ini" in logs
+    assert "ini = /etc/uwsgi/uwsgi.ini" in logs
+    assert "socket = /tmp/uwsgi.sock" in logs
+    assert "chown-socket = nginx:nginx" in logs
+    assert "chmod-socket = 664" in logs
+    assert "hook-master-start = unix_signal:15 gracefully_kill_them_all" in logs
+    assert "need-app = true" in logs
+    assert "die-on-term = true" in logs
+    assert "show-config = true" in logs
+    assert "wsgi-file = /app/main.py" in logs
+    assert "processes = 16" in logs
+    assert "cheaper = 2" in logs
+    assert "Checking for script in /app/prestart.sh" in logs
+    assert "Running script /app/prestart.sh" in logs
+    assert (
+        "Running inside /app/prestart.sh, you could add migrations to this file" in logs
+    )
+    assert "spawned uWSGI master process" in logs
+    assert "spawned uWSGI worker 1" in logs
+    assert "spawned uWSGI worker 2" in logs
+    assert "spawned uWSGI worker 3" not in logs
+    assert 'running "unix_signal:15 gracefully_kill_them_all" (master-start)' in logs
+    assert "success: nginx entered RUNNING state, process has stayed up for" in logs
+    assert "success: uwsgi entered RUNNING state, process has stayed up for" in logs
+
+
+def test_env_vars_1() -> None:
+    name = os.getenv("NAME", "")
+    dockerfile_content = generate_dockerfile_content_app_with_installs(name)
+    dockerfile = "Dockerfile"
+    response_text = get_response_text2()
+    sleep_time = int(os.getenv("SLEEP_TIME", 3))
+    remove_previous_container(client)
+    tag = "uwsgi-nginx-testimage"
+    test_path = Path(__file__)
+    path = test_path.parent / "app_with_installs"
+    dockerfile_path = path / dockerfile
+    dockerfile_path.write_text(dockerfile_content)
+    client.images.build(path=str(path), dockerfile=dockerfile, tag=tag)
+    container = client.containers.run(
+        tag, name=CONTAINER_NAME, ports={"80": "8000"}, detach=True
+    )
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    # Test that everything works after restarting too
+    container.start()
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    container.remove()

+ 87 - 0
uwsgi-nginx/tests/test_02_app/test_custom_nginx_app.py

@@ -0,0 +1,87 @@
+import os
+import time
+from pathlib import Path
+
+import docker
+import requests
+from docker.models.containers import Container
+
+from ..utils import (
+    CONTAINER_NAME,
+    generate_dockerfile_content_custom_nginx_app,
+    get_logs,
+    get_nginx_config,
+    get_response_text2,
+    remove_previous_container,
+)
+
+client = docker.from_env()
+
+
+def verify_container(container: Container, response_text: str) -> None:
+    response = requests.get("http://127.0.0.1:8080")
+    assert response.text == response_text
+    nginx_config = get_nginx_config(container)
+    assert "client_max_body_size 0;" not in nginx_config
+    assert "worker_processes 2;" in nginx_config
+    assert "listen 8080;" in nginx_config
+    assert "worker_connections 2048;" in nginx_config
+    assert "worker_rlimit_nofile;" not in nginx_config
+    assert "daemon off;" in nginx_config
+    assert "include uwsgi_params;" in nginx_config
+    assert "uwsgi_pass unix:///tmp/uwsgi.sock;" in nginx_config
+    assert "keepalive_timeout  300;" in nginx_config
+    logs = get_logs(container)
+    assert "getting INI configuration from /app/uwsgi.ini" in logs
+    assert "getting INI configuration from /etc/uwsgi/uwsgi.ini" in logs
+    assert "ini = /app/uwsgi.ini" in logs
+    assert "ini = /etc/uwsgi/uwsgi.ini" in logs
+    assert "socket = /tmp/uwsgi.sock" in logs
+    assert "chown-socket = nginx:nginx" in logs
+    assert "chmod-socket = 664" in logs
+    assert "hook-master-start = unix_signal:15 gracefully_kill_them_all" in logs
+    assert "need-app = true" in logs
+    assert "die-on-term = true" in logs
+    assert "show-config = true" in logs
+    assert "wsgi-file = /app/main.py" in logs
+    assert "processes = 16" in logs
+    assert "cheaper = 2" in logs
+    assert "Checking for script in /app/prestart.sh" in logs
+    assert "Running script /app/prestart.sh" in logs
+    assert (
+        "Running inside /app/prestart.sh, you could add migrations to this file" in logs
+    )
+    assert "spawned uWSGI master process" in logs
+    assert "spawned uWSGI worker 1" in logs
+    assert "spawned uWSGI worker 2" in logs
+    assert "spawned uWSGI worker 3" not in logs
+    assert 'running "unix_signal:15 gracefully_kill_them_all" (master-start)' in logs
+    assert "success: nginx entered RUNNING state, process has stayed up for" in logs
+    assert "success: uwsgi entered RUNNING state, process has stayed up for" in logs
+
+
+def test_env_vars_1() -> None:
+    name = os.getenv("NAME", "")
+    dockerfile_content = generate_dockerfile_content_custom_nginx_app(name)
+    dockerfile = "Dockerfile"
+    response_text = get_response_text2()
+    sleep_time = int(os.getenv("SLEEP_TIME", 3))
+    remove_previous_container(client)
+    tag = "uwsgi-nginx-testimage"
+    test_path = Path(__file__)
+    path = test_path.parent / "custom_nginx_app"
+    dockerfile_path = path / dockerfile
+    dockerfile_path.write_text(dockerfile_content)
+    client.images.build(path=str(path), dockerfile=dockerfile, tag=tag)
+    container = client.containers.run(
+        tag, name=CONTAINER_NAME, ports={"8080": "8080"}, detach=True
+    )
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    # Test that everything works after restarting too
+    container.start()
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    container.remove()

+ 86 - 0
uwsgi-nginx/tests/test_02_app/test_simple_app.py

@@ -0,0 +1,86 @@
+import os
+import time
+from pathlib import Path
+
+import docker
+import requests
+from docker.models.containers import Container
+
+from ..utils import (
+    CONTAINER_NAME,
+    generate_dockerfile_content_simple_app,
+    get_logs,
+    get_nginx_config,
+    get_response_text2,
+    remove_previous_container,
+)
+
+client = docker.from_env()
+
+
+def verify_container(container: Container, response_text: str) -> None:
+    response = requests.get("http://127.0.0.1:8000")
+    assert response.text == response_text
+    nginx_config = get_nginx_config(container)
+    assert "client_max_body_size 0;" in nginx_config
+    assert "worker_processes 1;" in nginx_config
+    assert "listen 80;" in nginx_config
+    assert "worker_connections 1024;" in nginx_config
+    assert "worker_rlimit_nofile;" not in nginx_config
+    assert "daemon off;" in nginx_config
+    assert "include uwsgi_params;" in nginx_config
+    assert "uwsgi_pass unix:///tmp/uwsgi.sock;" in nginx_config
+    logs = get_logs(container)
+    assert "getting INI configuration from /app/uwsgi.ini" in logs
+    assert "getting INI configuration from /etc/uwsgi/uwsgi.ini" in logs
+    assert "ini = /app/uwsgi.ini" in logs
+    assert "ini = /etc/uwsgi/uwsgi.ini" in logs
+    assert "socket = /tmp/uwsgi.sock" in logs
+    assert "chown-socket = nginx:nginx" in logs
+    assert "chmod-socket = 664" in logs
+    assert "hook-master-start = unix_signal:15 gracefully_kill_them_all" in logs
+    assert "need-app = true" in logs
+    assert "die-on-term = true" in logs
+    assert "show-config = true" in logs
+    assert "wsgi-file = /app/main.py" in logs
+    assert "processes = 16" in logs
+    assert "cheaper = 2" in logs
+    assert "Checking for script in /app/prestart.sh" in logs
+    assert "Running script /app/prestart.sh" in logs
+    assert (
+        "Running inside /app/prestart.sh, you could add migrations to this file" in logs
+    )
+    assert "spawned uWSGI master process" in logs
+    assert "spawned uWSGI worker 1" in logs
+    assert "spawned uWSGI worker 2" in logs
+    assert "spawned uWSGI worker 3" not in logs
+    assert 'running "unix_signal:15 gracefully_kill_them_all" (master-start)' in logs
+    assert "success: nginx entered RUNNING state, process has stayed up for" in logs
+    assert "success: uwsgi entered RUNNING state, process has stayed up for" in logs
+
+
+def test_env_vars_1() -> None:
+    name = os.getenv("NAME", "")
+    dockerfile_content = generate_dockerfile_content_simple_app(name)
+    dockerfile = "Dockerfile"
+    response_text = get_response_text2()
+    sleep_time = int(os.getenv("SLEEP_TIME", 3))
+    remove_previous_container(client)
+    tag = "uwsgi-nginx-testimage"
+    test_path = Path(__file__)
+    path = test_path.parent / "simple_app"
+    dockerfile_path = path / dockerfile
+    dockerfile_path.write_text(dockerfile_content)
+    client.images.build(path=str(path), dockerfile=dockerfile, tag=tag)
+    container = client.containers.run(
+        tag, name=CONTAINER_NAME, ports={"80": "8000"}, detach=True
+    )
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    # Test that everything works after restarting too
+    container.start()
+    time.sleep(sleep_time)
+    verify_container(container, response_text)
+    container.stop()
+    container.remove()

+ 63 - 0
uwsgi-nginx/tests/utils.py

@@ -0,0 +1,63 @@
+import os
+
+from docker.client import DockerClient
+from docker.errors import NotFound
+from docker.models.containers import Container
+
+CONTAINER_NAME = "uwsgi-nginx-test"
+
+
+def get_logs(container: Container) -> str:
+    logs = container.logs()
+    return logs.decode("utf-8")
+
+
+def get_nginx_config(container: Container) -> str:
+    result = container.exec_run(f"/usr/sbin/nginx -T")
+    return result.output.decode()
+
+
+def remove_previous_container(client: DockerClient) -> None:
+    try:
+        previous = client.containers.get(CONTAINER_NAME)
+        previous.stop()
+        previous.remove()
+    except NotFound:
+        return None
+
+
+def get_response_text1() -> str:
+    python_version = os.getenv("PYTHON_VERSION")
+    return f"Hello World from a default Nginx uWSGI Python {python_version} app in a Docker container (default)"
+
+
+def get_response_text2() -> str:
+    python_version = os.getenv("PYTHON_VERSION")
+    return f"Hello World from Nginx uWSGI Python {python_version} app in a Docker container"
+
+
+def generate_dockerfile_content_custom_app(name: str) -> str:
+    content = f"FROM tiangolo/uwsgi-nginx:{name}\n"
+    content += "COPY ./application /application\n"
+    content += "COPY ./prestart.sh /app/prestart.sh\n"
+    content += "WORKDIR /application\n"
+    return content
+
+
+def generate_dockerfile_content_custom_nginx_app(name: str) -> str:
+    content = f"FROM tiangolo/uwsgi-nginx:{name}\n"
+    content += "COPY app /app\n"
+    return content
+
+
+def generate_dockerfile_content_simple_app(name: str) -> str:
+    content = f"FROM tiangolo/uwsgi-nginx:{name}\n"
+    content += "COPY ./app/main.py /app/main.py\n"
+    return content
+
+
+def generate_dockerfile_content_app_with_installs(name: str) -> str:
+    content = f"FROM tiangolo/uwsgi-nginx:{name}\n"
+    content += "RUN pip install flask\n"
+    content += "COPY ./app/main.py /app/main.py\n"
+    return content