Wojtek @suda Siudzinski


Python/Node/Golang/Rust developer, DIY hacker, rookie designer, 3D print junkie. CEO @ Gaia Charge


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)()
comments powered by Disqus