DEV Community

Wojtek Siudzinski
Wojtek Siudzinski

Posted on • Originally published at suda.pl on

Deploying Django to production with uWSGI

There are many posts about dockerizing Django apps but I feel like there's some room for improvement.

Many of the approaches only focused on development flow, making the resulting image great for iterating but not ideal for production usage. Other ones, produced very big images, were running the application as root or ignored handling static files and deferred it to S3. With these images I had several goals:

  • Production-ready image (following general security tips)
  • Using alpine base for a small image
  • Multi-stage build for even smaller and cleaner image
  • Handling static files inside the same container

I decided on using uWSGI which actually also can serve static files, making my life much easier.

Note: I'm using pipenv to manage virtualenv and all dependencies (and I recommend you use it too), therefore some instructions might need tweaking if you're using bare pip.

Note 2: If you prefer running in ASGI, I wrote instructions for uvicorn based Docker image as well.

Instructions

Setup uWSGI

Start by adding it to your dependencies:

$ pipenv install uwsgi 

Then create uwsgi.ini file in your project root directory:

[uwsgi] chdir = /app uid = $(UID) gid = $(GID) module = $(UWSGI_MODULE) processes = $(UWSGI_PROCESSES) threads = $(UWSGI_THREADS) procname-prefix-spaced = uwsgi:$(UWSGI_MODULE) http-socket = :8080 http-enable-proxy-protocol = 1 http-auto-chunked = true http-keepalive = 75 http-timeout = 75 stats = :1717 stats-http = 1 offload-threads = $(UWSGI_OFFLOAD_THREADS) # Better startup/shutdown in docker: die-on-term = 1 lazy-apps = 0 vacuum = 1 master = 1 enable-threads = true thunder-lock = 1 buffer-size = 65535 # Logging log-x-forwarded-for = true # Avoid errors on aborted client connections ignore-sigpipe = true ignore-write-errors = true disable-write-exception = true no-defer-accept = 1 # Limits, Kill requests after 120 seconds harakiri = 120 harakiri-verbose = true post-buffering = 4096 # Custom headers add-header = X-Content-Type-Options: nosniff add-header = X-XSS-Protection: 1; mode=block add-header = Strict-Transport-Security: max-age=16070400 add-header = Connection: Keep-Alive # Static file serving with caching headers and gzip static-map = /static=/app/staticfiles static-map = /media=/app/media static-safe = /usr/local/lib/python3.7/site-packages/ static-gzip-dir = /app/staticfiles/ static-expires = /app/staticfiles/CACHE/* $(UWSGI_STATIC_EXPIRES) static-expires = /app/media/cache/* $(UWSGI_STATIC_EXPIRES) static-expires = /app/staticfiles/frontend/img/* $(UWSGI_STATIC_EXPIRES) static-expires = /app/staticfiles/frontend/fonts/* $(UWSGI_STATIC_EXPIRES) static-expires = /app/* 3600 route-uri = ^/static/ addheader:Vary: Accept-Encoding error-route-uri = ^/static/ addheader:Cache-Control: no-cache # Cache stat() calls cache2 = name=statcalls,items=30 static-cache-paths = 86400 # Redirect http -> https route-if = equal:${HTTP_X_FORWARDED_PROTO};http redirect-permanent:https://${HTTP_HOST}${REQUEST_URI} 

Note: The author of the config above is Diederik van der Boor 🙇

Create the Dockerfile

# Build argument allowing to change Python version ARG PYTHON_VERSION=3.7 # Build dependencies in a separate container FROM python:${PYTHON_VERSION}-alpine AS builder ENV WORKDIR /app COPY Pipfile ${WORKDIR}/ COPY Pipfile.lock ${WORKDIR}/ RUN cd ${WORKDIR} \  && pip install pipenv \  && pipenv install --system # Create the final container with the app FROM python:${PYTHON_VERSION}-alpine ENV USER=docker \ GROUP=docker \ UID=12345 \ GID=23456 \ HOME=/app \ PYTHONUNBUFFERED=1 WORKDIR ${HOME} # Create user/group RUN addgroup --gid "${GID}" "${GROUP}" \  && adduser \  --disabled-password \  --gecos "" \  --home "$(pwd)" \  --ingroup "${GROUP}" \  --no-create-home \  --uid "${UID}" \  "${USER}" # Run as docker user USER ${USER} # Copy installed packages COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages # Copy uWSGI binary COPY --from=builder /usr/local/bin/uwsgi /usr/local/bin/uwsgi # Copy the application COPY --chown=docker:docker . . # Collect static files RUN python manage.py collectstatic --noinput ENTRYPOINT ["uwsgi", "--ini", "uwsgi.ini"] EXPOSE 8080 

Start container

To work correctly, you need to set some environment variables. If you're using Docker Compose, you could use similar docker-compose.yml file:

version: "3" services: app: build: . image: foo/bar ports: - "8080:8080" environment: - UWSGI_MODULE=myapp.wsgi:application - UWSGI_PROCESSES=10 - UWSGI_THREADS=2 - UWSGI_OFFLOAD_THREADS=10 - UWSGI_STATIC_EXPIRES=86400 

then you can build the image with the following command:

$ docker-compose build app 

Conclusion

I'm quite happy with this setup and it has been working in production for some time. Please let me know if you have any comments and if I could improve this more!

Top comments (0)