Docker is most reliable and flexible way to distribute your Django applications, it able to include entire environment settings, dependencies and application files to deploy it to the server or any cloud provider.

This post show you a few examples how to build Docker image for your Django application, this will be based on Debian base images as it recommended option for Python applications, of course it could be build based on Alpine images but it mostly suitable for small apps because in most cases build time and final image size based on Alpine would be bigger than Debian based, more about issues with Alpine based images you can read here and here.

Django requires at least two containers to run in production mode, one serve application itself, second one is nginx container which proxy requests to app container and serves static files. To make it easier we will build basic images with minimal requirements first and then improve it gradually.

Building Basic Images

I propose you place docker related files under the .docker directory to keep it sorted. Example project structure looks as follows.

|-- .docker
|   |-- app
|   |   |-- Dockerfile
|   |   `-- entrypoint.sh
|   `-- nginx
|       |-- Dockerfile
|       `-- nginx.conf
|-- app
|   |-- __init__.py
|   |-- asgi.py
|   |-- settings.py
|   |-- urls.py
|   `-- wsgi.py
|-- docker-compose-app.yml
|-- docker-compose-nginx.yml
|-- docker-compose.yml
|-- manage.py
`-- requirements.txt

Application Image

First example is minimal image which may host Django in the out-of-box state with sqlite database and automatic migrations applying on container startup.

Dockerfile

FROM python:3.9-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONFAULTHANDLER=1 \
    PYTHONHASHSEED=random \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on \
    PIP_DEFAULT_TIMEOUT=100

# Copy app source
WORKDIR /app
COPY . .

# Install requirements
RUN pip install --no-cache-dir gunicorn gevent
RUN pip install --no-cache-dir -r requirements.txt

# Copy and enable
COPY .docker/app/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]

EXPOSE 8000
# 'app' in the 'app.wsgi:application' - it is directory name where placed settings.py file
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "--error-logfile", "-", "--access-logfile", "-", "app.wsgi:application"]

entrypoint.sh

#!/bin/bash
set -e

echo "Applying database migrations..."
python manage.py migrate

exec "$@"

docker-compose-app.yml

Use this docker compose to build image with given files structure.
Build command: docker-compose -f docker-compose-app.yml build

version: '3.8'

services:
  django_app:
    container_name: django_app
    build:
      context: .
      dockerfile: .docker/app/Dockerfile
    image: django_app:latest
    restart: always
    ports:
      - "80:8000"

Nginx image

Nginx serves staticfiles and proxying http requests to the WSGI server which is required in produciton mode (DEBUG=False)

Dockerfile

FROM django_app:latest as app_image

# Generate staticfiles bundle
RUN python manage.py collectstatic --noinput

FROM nginx:alpine

# Copy staticfiles bundle to the image
COPY --from=app_image /app/staticfiles /var/www/static
COPY .docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf

nginx.conf

server {
    listen 80;
    root /var/www/static;
    error_page 500 502 503 504 /nginx_50x.html;
    set_real_ip_from 172.16.0.0/12;
    set_real_ip_from 192.168.0.0/16;
    real_ip_header X-Forwarded-For;
    client_max_body_size 64M;

    location / {
      try_files $uri @proxy;
    }

    location @proxy {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://django_app:8000;  # << Application container name
      proxy_connect_timeout       900;
      proxy_send_timeout          900;
      proxy_read_timeout          900;
      send_timeout                900;
    }

    location /static {
      autoindex off;
      alias /var/www/static;
    }
}

docker-compose-nginx.yml

Use this docker compose to build image with given files structure.
Build command: docker-compose -f docker-compose-nginx.yml build

version: '3.8'

services:
  django_nginx:
    container_name: django_nginx
    restart: always
    build:
      context: .
      dockerfile: .docker/nginx/Dockerfile
    image: django_nginx
    ports:
        - "80:80"

Full project example could be found on Github

Postgres Ready Application Image

This example shows how to install psycopg2 dependencies, this is single change from basic image, everything else remains same. You can find full example on Github

Dockerfile

FROM python:3.9-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONFAULTHANDLER=1 \
    PYTHONHASHSEED=random \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on \
    PIP_DEFAULT_TIMEOUT=100

# Install dependencies
RUN apt update && apt install -y \
    # psycopg2 dependencies
    libpq-dev gcc \
    # Clean apt cache
    && apt clean && rm -rf /var/lib/apt/lists/*

# Copy app source
WORKDIR /app
COPY . .

# Install requirements
RUN pip install --no-cache-dir gunicorn gevent
RUN pip install --no-cache-dir -r requirements.txt

# Copy and enable
COPY .docker/app/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]

EXPOSE 8000
# 'app' in the 'app.wsgi:application' - it is directory name where placed settings.py file
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "--error-logfile", "-", "--access-logfile", "-", "app.wsgi:application"]

MySQL Ready Application Image

Same as previous but with MySQL dependencies installation. Full example available on Github

FROM python:3.9-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONFAULTHANDLER=1 \
    PYTHONHASHSEED=random \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on \
    PIP_DEFAULT_TIMEOUT=100

# Install dependencies
RUN apt update && apt install -y \
    # mysqlclient dependencies
    default-libmysqlclient-dev gcc \
    # Clean apt cache
    && apt clean && rm -rf /var/lib/apt/lists/*

# Copy app source
WORKDIR /app
COPY . .

# Install requirements
RUN pip install --no-cache-dir gunicorn gevent
RUN pip install --no-cache-dir -r requirements.txt

# Copy and enable
COPY .docker/app/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]

EXPOSE 8000
# 'app' in the 'app.wsgi:application' - it is directory name where placed settings.py file
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "--error-logfile", "-", "--access-logfile", "-", "app.wsgi:application"]

Conclusion

This is good start to your Docker environment for the Django application. In case if your database would be dockerized too (as it in the example repository’s docker-compose.yml) would be good to add “database wait” script to make application’s image entrypoint.sh more flexible to wait until database will be ready to accept incoming connections before entrypoint.sh executes migrations, it avoid startup errors while database isn’t started yet, I’ll tell about this in next post.