Skip to main content

Django + Gunicorn + nginx — the complete guide

TL;DR

The definitive guide to configuring Django with Gunicorn and nginx for production Linux deployments.

Key facts

Topology
Django + Gunicorn + nginx

TL;DR

Django with Gunicorn and nginx is the most widely deployed Python web stack. Gunicorn runs your Django application as a WSGI server, nginx sits in front as a reverse proxy handling SSL, static files, and connection buffering.

Architecture overview

Client → nginx (TLS, static files, buffering) → Gunicorn (WSGI) → Django (application)

nginx handles what it does best: SSL termination, serving static assets, and buffering slow clients. Gunicorn handles what it does best: running Python code across multiple worker processes.

Gunicorn configuration

Create gunicorn.conf.py in your project root:

import multiprocessing

bind = 'unix:/run/gunicorn.sock'
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'gthread'
threads = 4
timeout = 120
graceful_timeout = 30
keepalive = 5
max_requests = 1000
max_requests_jitter = 50
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'

Key settings explained:

  • workers: The formula 2 * CPUs + 1 is the Gunicorn recommendation for typical workloads
  • worker_class = 'gthread': Thread-based workers handle I/O-bound Django views well
  • max_requests: Recycles workers after 1000 requests to mitigate slow memory leaks
  • max_requests_jitter: Prevents all workers from restarting simultaneously

Unix socket vs TCP port

Use a Unix socket (unix:/run/gunicorn.sock) when nginx and Gunicorn are on the same machine — it avoids TCP overhead and is not accessible from the network. Use TCP (0.0.0.0:8000) only when nginx runs on a separate server.

nginx configuration

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 10M;

    location /static/ {
        alias /var/www/myapp/staticfiles/;
        expires 365d;
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /var/www/myapp/media/;
        expires 30d;
    }

    location / {
        proxy_pass http://unix:/run/gunicorn.sock;
        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;
        proxy_redirect off;
    }
}

Static files

Run collectstatic during every deployment:

python manage.py collectstatic --noinput

Always serve static files through nginx — never through Gunicorn. Django's STATIC_ROOT should point to the directory nginx serves via the /static/ alias.

Graceful restarts

Reload Gunicorn without dropping requests:

sudo systemctl reload gunicorn

This sends a HUP signal to the master process, which gracefully cycles workers one at a time.

With Reflex

Reflex provides unified monitoring across the Django–Gunicorn–nginx stack. It tracks worker health, request latency, static file performance, and database query times. During deployments, Reflex orchestrates graceful Gunicorn reloads, runs migrate and collectstatic, and verifies the application is healthy before marking the deploy complete. See How it works.