Containerize a Django application

Archie To

It is common and considered good pratice for applications to be containerized in production environments. Conterization allows apps to run in a separate environment from the host machine, which results in performance reliability, to implement multiple micro services that work closely with each other, and to be shipped with ease. In this article, Archie will show a surface-level way of how a Django application can be containerized with Docker.

Assumptions

This article assumes that readers have a good basic understanding of Django, Docker and containerization concepts. To learn more about Django, please visit their documentation. For more about Docker, please have a look at a beginner-friendly article written by our amazing team member, Karan.

The big picture

In a production environment, a Django app needs 2 main containers:

  • A container running the Django app itself, either as an asgi (e.g. Daphne) or wsgi server (e.g. gunicorn). This server receives a request, run the Python code and returns a response.
  • A container running a webserver such as Nginx to serve static files and acts as a reverse proxy for the Django app.

In many cases, other containers are required. For example, a Postgres container to store data for the app, or a Redis container to provide access to hot data.

For our case, since all applications are deployed on STRAP, they will have access to an external Postgres database.

Django app container

Here are the requirements for our Django container:

  • Includes all necesary packages to run the app
  • Includes the code to run the app
  • Runs the app as a non-root user
  • Performs necessary actions and starts up the app server

With those requirements, we comes up with the following Dockerfile:

# Official base image
FROM python:3.10.10-alpine

# Set work directory
WORKDIR /usr/src/app

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

# Copy project
COPY . .

# Remove dir that stores Django collected static files
# as these files are served by Nginx container
# NOTE: You only need this step if you run "python manage.py collectstatic" and it creates a directory somewhere in your app directory
RUN rm -rf /usr/src/app/<path_to_staticfiles_dir>

# Create a new user and set this user as the owner of the work directory
RUN adduser -D app-user
RUN chown -R app-user /usr/src/app
USER app-user

# Run migration and start WSGI server
ENTRYPOINT ["sh", "deployments/start.sh"]

Here is our start.sh to perform necessary actions and starts up the app server:

#!/bin/sh
# Migrate schema to the database
python <app_name>/manage.py migrate

# Start Gunicorn server on port 8000 (port is of your choice)
gunicorn -w 4 --chdir /usr/src/app/<app_name> <app_name>.wsgi:application --bind 0.0.0.0:8000 --timeout 1000

Nginx container

Here are the requirements for our Nginx containers:

  • Includes static files for our Django app
  • Sets up routes to serve our Django app and the static files
  • Runs the webserver as a non-root user
  • Exposes a port that can be accessed by external requests

With those requirements, we comes up with the following Dockerfile:

# Use the official Nginx image as a parent image
FROM nginx:1.25.3

# Replace the default server config with our custom config
RUN rm /etc/nginx/conf.d/default.conf
COPY ./deployments/nginx/nginx.conf /etc/nginx/conf.d

# Copy Django collected static files
RUN mkdir -p /usr/src/app/<app_name>/static
COPY <path_to_staticfiles_dir> /usr/src/app/<app_name>/static/

# Create a new user and set uid to 1000
# You can choose whatever user id you want or no id at all
RUN adduser --disabled-login --gecos '' app-nginx && \
    usermod -u 1000 app-nginx && \
    groupmod -g 1000 app-nginx

# Create a directory for the Nginx process and set permissions
RUN mkdir -p /var/run/nginx \
    && chown -R app-nginx:app-nginx /var/run/ \
    && chown -R app-nginx:app-nginx /var/cache/nginx \
    && chown -R app-nginx:app-nginx /etc/nginx/conf.d

# Switch to a non-root user
USER app-nginx

# Expose a non-privileged port
EXPOSE 8080

# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

Here is our custom server configuration nginx.conf:

upstream django_app {
    server <django_app_container_name>:8000;
}

server {

    listen 8080;

    location / {
        proxy_pass http://django_app;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /usr/src/app/<app_name>/static/;
    }
}

Setup for development

A process to build and run the containers in your development server might look as follows:

# Collect Django static files into one directory
python <app_name>/manage.py collectstatic --no-input

# Build the Django container
docker build -f Dockerfile -t <app_name>-django:latest .

# Build the Nginx container
docker build -f nginx/Dockerfile -t <app_name>-nginx:latest .

# Stand the containers up
docker-compose -f docker-compose.yml up -d

Note that you will have to fix the file paths depending on the setup for your project.

Your docker-compose.yml might look as follow:

version: "3.8"

services:
<app_name>_django:
    container_name: <app_name>-django
    image: <app_name>-django:latest
    env_file:
    - <path_to_env_file>
    expose:
    - "8000"

<app_name>_nginx:
    container_name: <app_name>-nginx
    image: <app_name>-nginx:latest
    ports:
    - 1337:8080
    depends_on:
    - zoodb_django

In production, if you are deploying your app as a pod in a K8s cluster, you can deploy these two containers into that same pod. In this case, the two containers will share a localhost network to talk to each other. In the future, we might have an article that talks more about deploying Django app onto Kubernetes.