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 formula2 * CPUs + 1is the Gunicorn recommendation for typical workloadsworker_class = 'gthread': Thread-based workers handle I/O-bound Django views wellmax_requests: Recycles workers after 1000 requests to mitigate slow memory leaksmax_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.