Ultimate Guide for Creating a Production Django Container

Today we’re going to set up a proper container to deploy a Django application. We’re using the following tools: Docker, Nginx, and Gunicorn.

Another Django + Docker Guide?

Even thought there are many resources out there, I decided to make this post since most of the resources online for how to do this are straight up garbage. I’ve seen 2 crucial common mistakes or things that I disagree with:

1. They run the development server of Django in the container

This is so wrong on so many levels. I think why this happens is people writing these are lazy. Learning about how to get it running with Gunicorn and a Nginx web server is harder to grasp.

2. They run an nginx instance in a different container

This is more personal preference. I see limited benifit in running two containers for the same application. Unless your Nginx reverse proxy is serving other applications, why make the application more complicated by having to debug 2 containers for a single application?

Additional Resources

This is the single best resource I have come across for deploying Django to production. It goes in depth into details about why you need certain production settings and overall some solid background about deploying production applications on a linux machine.

It does not cover containerizing an application and assumes that you’ll be deploying on a bare OS. We leverage a lot of these tips in our docker image setup.

Project Setup

All my Django applications follow a similar structure and setup that I’ll provide below. The overall structure is stored in a git repository and the continous deployment pipelines are run against this.

Overall Structure

The following is the layout of my top level git directory for each of my Django Application projects. We have our Django Application and have an Nginx in thier own sub-directories. the dockerfile and scripts we use for setup live in the root.

├── djangoApp/       # This directory contains our Django project and code
├── dockerfile       # Dockerfile we'll be building
├── nginx/            # Directory of Nginx related configurations
└── start.sh         # Improtant startup scripts

Django Project Setup

I use a pretty standard setup similar to the Django polls tutorial. We won’t be going into how to set up a Django project. I assume that you already have a Django project that you can run a development version of. Make sure you generate a requirements file of your dependencies.

There are a few things we’re going to need to setup in our settings.py file to get the project working as a container.

Variable Setup

First, I change my Django variables to be environment variables. That allows us configure our application for development and production. Mine usually looks like the following:

 # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-super-secret-key')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'True').lower()  == 'true'
allowed_host = os.getenv('ALLOWED_HOST', 'localhost')
ALLOWED_HOSTS = [allowed_host]
csrf_trusted_origin = os.getenv('CSRF_TRUSTED_ORIGIN', "http://localhost:8000")
CSRF_TRUSTED_ORIGINS = [csrf_trusted_origin]

Static and Media Files Configruation

We want Nginx to serve our static files instead of our expensive python process. Let’s hook it up. This will keep our development environment working as is.

This is the static file setup.

STATIC_URL = '/static/'
STATIC_ROOT = '/var/cache/djangoApp/static/'

STATICFILES_DIRS = [
    BASE_DIR / "static",
]

We also want to do the same for media files. These are files that the users upload and view.

MEDIA_ROOT = '/var/opt/djangoApp/media/'
MEDIA_URL = '/media/'

Introducing Gunicorn Web Server

Gunicorn is how we serve our Django application in production. It’s pretty configurable and theres a ton of documentation on it so I wont go into details. See the Initializing Our Application section for where we actually use this.

To start a basic gunicorn web server instance, we use the following command.

gunicorn --bind=0.0.0.0:8000 \
    djangoApp.wsgi:application

Setting Up Nginx

Creating an Nginx configuration is pretty simple for a one application setup like this. We just need a location block that proxies to our Gunicorn webserver. We also add static and modia blocks to serve files from our fast Nginx web server. Nginx is very powerful and can solve other issues for you. I talked about solving the DisallowedHost exceptions using Nginx.

server {
  listen 80; # Add 443 for SSL support
  server_name djangoApp.com;


  location / {
      proxy_pass http://localhost:8000;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;
      proxy_redirect off;
      client_max_body_size 100M;
  }

  location /static/ {
      alias /var/cache/djangoApp/static/;
  }

  location /media/ {
      alias /var/opt/djangoApp/media/;
  }
}

Creating our Dockerfile

So the first thing we’re going to need is a Dockerfile to define what our container is going to look like. We’re going to be using a multi-build docker image for this. Don’t worry we’re going to break it down step by step.

Basic Dockerfile

We’re going to start by exploring the multibuild dockerfile. We have a build step that downloads all of our python(pip in this instance) depenedencies. Once that is done, we move to our final image, here is where we copy over our python files and then install the dependencies from the builder steps.

We’ll launch our Django app using the runserver command which runs the development django server. We do NOT want to run this image. I just want to demo with a simpler dockerfile how the multistage build works.

I like to create an environement variable with the name of our project and use that for setting up directories:

ENV PROJECT_NAME=djangoApp

Here’s our starter dockerfile:

###########
# BUILDER #
###########

# pull official base image
FROM python:3.12-slim-bullseye as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc

# install python dependencies
COPY ./djangoAppApp/requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt

#########
# FINAL #
#########

# pull official base image
FROM python:3.12-slim-bullseye

# create the appropriate directories
ENV PROJECT_NAME=djangoApp

# Program Files
RUN mkdir -p /opt/$PROJECT_NAME

# Install our spplaicaiont
WORKDIR /opt/$PROJECT_NAME
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*

RUN ["python", "manage.py", "runserver"]

Making Our Dockerfile Production Like

We went ahead and set up our Ngnix configuration and talked about Gunicorn. How about we use it instead of rellying on our development server? I’m going to talk about a few specifics that we’re adding. You can jump to the end of the section if you want the complete dockerfile.

Nginx Configruations

We need to do a few steps in our dockerfile to setup our Nginx webserver. First we need to install Nginx. I go with the nginx-light package since it has everything we need while being more lightweight than the full Nginx.

RUN apt-get update && apt-get install -y --no-install-recommends netcat nginx-light

Next we’re going to copy in our configuration to the Nginx directory. Since a default files already exists we need to go ahead and remove that. I found this easier than working with them. In other tutorials, you may find copying your configuration and adding a link to the sites-enabled and sites-avaliable directories is required.

# Setup NGINX
RUN rm /etc/nginx/sites-available/default
RUN rm /etc/nginx/sites-enabled/default
COPY nginx/nginx.conf /etc/nginx/conf.d

The last thing we need to do is create directories for our media and static files. This is required since these files are being directly served by Nginx and not by our Django process.

# Media Files
RUN mkdir -p /var/opt/$PROJECT_NAME/media
# Static Files
RUN mkdir -p /var/cache/$PROJECT_NAME/static

Our static directory gets populated later on in the script with the following command

# Populate static data into nginx static dir
RUN python manage.py collectstatic --no-input --clear

Guinicorn Setup

Guincorn luckily does not need as much to get configured. It needs to be installed and logs need to be setup.

Heres how we setup our logs

# Log Files
RUN mkdir -p /var/log/$PROJECT_NAME
RUN touch /var/log/$PROJECT_NAME/gunicorn.log

Installation is a basic pip command

RUN pip install gunicorn

The Production Dockerfile

Lets go ahead and put all of the pieces together. We’ll have this spawling image:

###########
# BUILDER #
###########

# pull official base image
FROM python:3.12-slim-bullseye as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc

# install python dependencies
COPY ./djangoAppApp/requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt

#########
# FINAL #
#########

# pull official base image
FROM python:3.12-slim-bullseye

# install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends netcat nginx-light

# create the appropriate directories
ENV PROJECT_NAME=djangoApp

# Setup NGINX
RUN rm /etc/nginx/sites-available/default
RUN rm /etc/nginx/sites-enabled/default
COPY nginx/nginx.conf /etc/nginx/conf.d

# Program Files
RUN mkdir -p /opt/$PROJECT_NAME
# Media Files
RUN mkdir -p /var/opt/$PROJECT_NAME/media
# Static Files
RUN mkdir -p /var/cache/$PROJECT_NAME/static
# Log Files
RUN mkdir -p /var/log/$PROJECT_NAME
RUN touch /var/log/$PROJECT_NAME/gunicorn.log
RUN touch /var/log/$PROJECT_NAME/$PROJECT_NAME.log

# Install our spplaicaiont
WORKDIR /opt/$PROJECT_NAME
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*
RUN pip install gunicorn

COPY ./djangoAppApp /opt/$PROJECT_NAME

RUN python -m compileall \
    /etc/opt/$PROJECT_NAME

# Do a database migration - This doesn't work in the mounted volume
# RUN python manage.py  migrate --noinput

# Populate static data into nginx static dir
RUN python manage.py collectstatic --no-input --clear

# Setup that can start gunicorn and nginx
ADD start.sh .
RUN chmod +x start.sh

ENTRYPOINT [ "./start.sh" ]

Initializing Our Application

You may have noticed the start.sh file thoughout this post. This is our entrypoint for starting the container. It does the following:

  • Starts our Nginx web server
  • Starts our Gunicorn web server

That looks like the following as a sh script:

#!/bin/sh
service nginx start; gunicorn --workers=2 \
    --log-file=/var/log/djangoApp/gunicorn.log \
    --bind=0.0.0.0:8000 \
    djangoApp.wsgi:application

So everytime when the container fires up it launches both webservers and you’re good to go.

Finishing Up

Things should be working great. There are a few things you may need to do if you’re running into errors.

Manual Setup Steps

Lets say you try and use this application, what you’re going to find is that it does not work. That is because our database schema has not been properly setup yet. We can fix that in a few manual steps:

  1. Exec into the container

    docker exec -it {container-id} /bin/sh
    
  2. Run migrations

    python manage.py migrate
    
  3. Bonus: Create a Superuser

    python manage.py createsuperuser
    

Definetly Not Static

These are most or all the steps and pieces you’re going to need to setup a Django application using Docker. These configurations and tools can be swapped out for other things, such as Apache for Nginx web server or using uwsgi instead of Gunicorn. However the process will be similar.

Once you’ve containerized you application, you may be interested in building your container in a Github Actions Pipeline or serving your new Django container via NixOS. I write about devops, infrastructure and Django so stay tuned for more content like this.

More sweet content?

Stay up to date!

I don't sell your data. Read my privacy policy .

Related Articles