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"
.
{% if cookiecutter.use_fathom == "y" -%}
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
{% endif -%}
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 . .
{%- if cookiecutter.use_fathom == "y" %}
# Django needs a copy of the staticfiles.json manifest file.
COPY --from=build-stage --chown=runner:runner /workdir/static/staticfiles.json /workdir/static/staticfiles.json
{%- endif %}
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 }}"]
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 heartbeat 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.