Production-ready Django 3 ASGI Docker image
Tags: django, asgi, docker, async
Published: 2020-01-02 17:01:19

---
title: Production-ready Django 3 ASGI Docker image
published: true
date: 2020-01-02 16:49:49 UTC
tags: django,asgi,docker,async
canonical_url: https://suda.pl/deploying-django-3-asgi/
---

 ![Production-ready Django 3 ASGI Docker image](https://suda.pl/content/images/2020/01/spacex-OHOU-5UVIYQ.jpg)

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

With [Django 3 released](https://docs.djangoproject.com/en/3.0/releases/3.0/#asgi-support), 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](https://suda.pl/deploying-django-with-uwsgi/) 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](https://github.com/tiangolo/uvicorn-gunicorn-docker). One thing missing was the ability to serve static files, but here's where [`WhiteNoise`](http://whitenoise.evans.io/en/stable/base.html) comes in.

Let's jump in to the setup.

# Instructions

Compared to my [uWSGI setup](https://suda.pl/deploying-django-with-uwsgi/), here we just need to create a `Dockerfile`:

```docker
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`](http://whitenoise.evans.io/en/stable/base.html) which needs to be added to dependencies:

```sh
$ pipenv install whitenoise
```

and then added `whitenoise.middleware.WhiteNoiseMiddleware` to the top of the `MIDDLEWARE` array in `settings.py`, right below the `SecurityMiddleware`:

```python
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](http://whitenoise.evans.io/en/stable/index.html#shouldn-t-i-be-pushing-my-static-files-to-s3-using-something-like-django-storages):

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

```python
"""
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](https://docs.djangoproject.com/en/3.0/topics/async/), means you should not use possibly blocking, synchronous methods (like the ORM) in the main thread. [Quick tip](https://forum.djangoproject.com/t/is-there-a-way-to-disable-the-synchronousonlyoperation-check-when-using-the-orm-in-a-jupyter-notebook/548/3) here is, that if you get `SynchronousOnlyOperation` exception you might want to wrap it with `sync_to_async`:

```python
from asgiref.sync import sync_to_async

def my_db_function():
    <do orm stuff>

await sync_to_async(my_db_function)()
```
<!--kg-card-end: markdown-->