Production-ready Django 3 ASGI Docker image
With Django 3 released, it's a great moment to jump in on all the async goodies it provides. Unfortunately for me, it means dropping my uWSGI Docker config and figuring out a new approach.
Fortunately, Sebastián Ramírez of fastapi
fame, created a very nice base image using uvicorn
: uvicorn-gunicorn-docker. One thing missing was the ability to serve static files, but here's where WhiteNoise
comes in.
Let's jump in to the setup.
Instructions
Compared to my uWSGI setup, here we just need to create a Dockerfile
:
ARG PYTHON_VERSION=3.7
# Build dependencies in separate container
FROM tiangolo/uvicorn-gunicorn:python${PYTHON_VERSION}-alpine3.8 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 tiangolo/uvicorn-gunicorn:python${PYTHON_VERSION}-alpine3.8
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 the application
COPY --chown=docker:docker . .
# Collect the static files
RUN python manage.py collectstatic --noinput
Serving the static files
For this purpouse, I'm using WhiteNoise
which needs to be added to dependencies:
$ pipenv install whitenoise
and then added whitenoise.middleware.WhiteNoiseMiddleware
to the top of the MIDDLEWARE
array in settings.py
, right below the SecurityMiddleware
:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# ...
]
Side note: I know many people are wondering why I'm so set on serving static files, instead of just using S3 buit I think David explained it well:
Shouldn’t I be pushing my static files to S3 using something like Django-Storages?
No. (...) problem with a push-based approach to handling static files is that it adds complexity and fragility to your deployment process.
Running the image
uvicorn
needs to know what is the module is the main application, therefore we need to set the APP_MODULE
environment variable to myapp.asgi:application
replacing myapp
with the name of your app. This assumes you have the asgi.py
file (ASGI equivalent of the wsgi.py
) which will be generated automatically when creating new Django 3.0 project but if you're migrating from an older version, you can use this one:
"""
ASGI config for myapp project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings')
application = get_asgi_application()
Notes
Running in async mode, means you should not use possibly blocking, synchronous methods (like the ORM) in the main thread. Quick tip here is, that if you get SynchronousOnlyOperation
exception you might want to wrap it with sync_to_async
:
from asgiref.sync import sync_to_async
def my_db_function():
<do orm stuff>
await sync_to_async(my_db_function)()