Deploying Django to production with uWSGI
Tags: django, uwsgi, docker, python
Published: 2020-03-30 09:49:12

---
title: Deploying Django to production with uWSGI
published: true
date: 2019-09-01 12:28:00 UTC
tags: django,uwsgi,docker,python
canonical_url: https://suda.pl/deploying-django-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`](https://medium.com/goglides/stop-running-an-application-inside-a-docker-container-as-the-root-user-f255a7810c0c) 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](https://docs.docker.com/develop/develop-images/multistage-build/) build for even smaller and cleaner image
- Handling static files inside the same container

I decided on using [`uWSGI`](https://uwsgi-docs.readthedocs.io/en/latest/) which actually also can serve static files, making my life much easier.

**Note:** I'm using [pipenv](https://pipenv.readthedocs.io/en/latest/) 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](https://dev.to/suda/production-ready-django-3-asgi-docker-image-1d49) as well.

# Instructions

## Setup `uWSGI`

Start by adding it to your dependencies:

```sh
$ pipenv install uwsgi
```

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

```ini
[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](https://github.com/nginxinc/kubernetes-ingress/issues/143#issuecomment-347814243) 🙇

## Create the `Dockerfile`

```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:

```yaml
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:

```sh
$ 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!

<!--kg-card-end: markdown-->