Dockerfile for Django

Add one Dockerfile for the Django project, replacing core.wsgi if needed and {{ cookiecutter.project_slug }}, and another for static files.

Warning

When deploying Docker, remember to set the number of workers using a environment variable like GUNICORN_CMD_ARGS="--workers 3".

Dockerfile_django
FROM python:{{ cookiecutter.python_version }}

RUN apt-get update && apt-get install -y --no-install-recommends \
      gettext \
   && rm -rf /var/lib/apt/lists/*

RUN groupadd -r runner && useradd --no-log-init -r -g runner runner

COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt

WORKDIR /workdir
USER runner:runner
COPY --chown=runner:runner . .

ENV DJANGO_ENV=production
ENV WEB_CONCURRENCY=2

RUN python manage.py compilemessages

EXPOSE 8000
CMD ["gunicorn", "core.wsgi", "--bind", "0.0.0.0:8000", "--worker-tmp-dir", "/dev/shm", "--threads", "2", "--name", "{{ cookiecutter.project_slug }}"]
Dockerfile_static
FROM python:{{ cookiecutter.python_version }} as build-stage

COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt

WORKDIR /workdir
COPY . .

ENV DJANGO_ENV=production

RUN python manage.py collectstatic --noinput -v2

FROM nginxinc/nginx-unprivileged:latest as production-stage
USER root
COPY --from=build-stage --chown=nginx:root /workdir/static /usr/share/nginx/html/static
COPY --chown=nginx:root default.conf /etc/nginx/conf.d/default.conf
USER nginx

Gunicorn

Worker class

Gunicorn describes use cases where asynchronous workers are preferred. In particular, check whether the application makes long blocking calls and is therefore I/O bound.

Note

If Gunicorn is deployed behind a proxy server, like Apache, then it isn’t “serving requests directly to the internet.” That said, check whether other applications that send requests behind the proxy server are likely to DOS the application (if it were to use a synchronous worker).

If not, then the synchronous worker classes (sync and gthreads) are preferred.

Note

The gthreads worker class is synchronous, despite appearing under AsyncIO Workers.

When using the sync worker class, the --timeout option behaves like a request timeout, because the worker can only either handle the request or handle the heartbeat. (Gunicorn otherwise has no option like uWSGI’s harakiri for request timeouts.)

When using the gthreads worker class, a main thread handles the heartbeat. As such, we use the gthreads worker class, to not have to worry about request timeouts.

Note

Setting the --threads option to more than 1 automatically sets the worker class to gthreads.

Number of threads

Ensure your code is thread safe. Notably, psycopg2 cursors are not thread safe, though this isn’t a concern for typical usage of Django.

When using threads, the application is loaded by the worker and some memory is shared between its threads (thus consuming less memory than additional workers would).

Unless the server becomes memory-bound, use a minimum number of threads (2) and instead increase the number of workers, to lower the risk around thread safety.

Note

If the application is CPU-bound, additional threads don’t help, due to Python’s GIL. Instead, add additional workers (up to twice the number of cores).

Concurrency

cores * 2 + 1 is the recommended number of workers + threads. However, multiple applications on the same server need to share the same cores – plus, the server might not be dedicated to Gunicorn.

At build time, the mix of applications and number of cores are unknown. As such, we omit the --workers option (highest level of precedence), and set a WEB_CONCURRENCY environment variable (lowest level). Docker Compose can then set a GUNICORN_CMD_ARGS=”--workers 3” environment variable to override the number of workers.

The template sets WEB_CONCURRENCY=2 and --threads 2 (as described in the previous section), such that the total concurrency is 4 – one more than cores * 2 + 1 for a single core.

Signals

The shell form, CMD command param1, runs the command as a subcommand of /bin/sh -c, which doesn’t pass signals. For Gunicorn to receive the SIGTERM signal and stop gracefully, the exec form, CMD ["command", "param1"], is used.

Reference: Signal Handling

Other options

--bind 0.0.0.0:8000 uses Docker’s default bind address for containers and Gunicorn’s default port.

--worker-tmp-dir /dev/shm avoids a potential issue.

--name PROJECTNAME helps to distinguish processes of different applications on the same server.

Additional options can be configured from Docker Compose using the GUNICORN_CMD_ARGS environment variable, though command-line arguments take precedence.

Troubleshooting

Idle workers are regularly killed. As such, it can be hard to debug what happened. See this FAQ question for some guidance.