Django in Docker for Production with Gunicorn and Nginx is less about running Django and more about orchestrating a small, resilient web service.

Let’s see this in action. Imagine you have a Django app and you want to deploy it.

First, you’ll need a Dockerfile for your Django app. This is where Gunicorn lives.

FROM python:3.9-slim-buster

WORKDIR /app

COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /app/

ENV PYTHONUNBUFFERED 1

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "your_project.wsgi:application"]

This Dockerfile builds an image that installs your app’s dependencies, copies your code, and then starts Gunicorn. Gunicorn is your production-ready WSGI server, designed to handle multiple requests efficiently. The CMD line tells Docker to run Gunicorn, binding it to all network interfaces on port 8000 within the container.

Next, you need a docker-compose.yml to bring Nginx into the picture. Nginx will act as a reverse proxy.

version: '3.8'

services:
  web:
    build: .
    expose:
      - 8000
    volumes:
      - static_volume:/app/static
    command: gunicorn --bind 0.0.0.0:8000 your_project.wsgi:application

  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - static_volume:/app/static
    depends_on:
      - web

volumes:
  static_volume:

This docker-compose.yml defines two services: web (your Django app with Gunicorn) and nginx. The web service builds from your Dockerfile. expose makes port 8000 available to other services in the Docker network, but not directly to the host. static_volume is crucial for serving static files. The nginx service uses the official Nginx image. ports maps port 80 on your host to port 80 in the container, making your app accessible. The nginx.conf volume mounts your custom Nginx configuration. depends_on ensures Nginx starts after the web service.

Here’s a sample nginx.conf:

server {
    listen 80;
    server_name localhost;

    location /static/ {
        alias /app/static/;
    }

    location / {
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

This Nginx configuration tells it to listen on port 80. For requests starting with /static/, it serves files directly from the /app/static/ directory within the container. For all other requests, it proxies them to the web service (which is Gunicorn running on port 8000) and forwards important headers.

To make this work, you need to configure Django to collect static files. In your settings.py:

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'static'

And then run python manage.py collectstatic within your Django container before Nginx can serve them. A common pattern is to build a separate image for your Django app that includes the collectstatic step, or to run it as a one-off command in your Docker Compose setup.

The static_volume in docker-compose.yml is key. When collectstatic runs in the web container, it places files into /app/static. This directory is then mounted as a volume, and Nginx, also configured to look in /app/static (via the static_volume mount and the alias directive in nginx.conf), can serve these files directly. This bypasses Gunicorn for static assets, which is much more efficient.

The real power here is that Nginx handles SSL termination, load balancing (if you were to scale the web service), and serving static files efficiently, while Gunicorn focuses solely on serving your dynamic Django application.

What most people don’t realize is that the proxy_pass http://web:8000; directive in Nginx uses Docker’s internal DNS resolution. web is the service name defined in docker-compose.yml, and Docker automatically resolves it to the correct IP address of the running web container on the internal network.

The next thing you’ll likely want to tackle is managing database migrations and running background tasks.

Want structured learning?

Take the full Django course →