Nginx Production Checklist for Django
A production checklist for Nginx in front of Django: server blocks and redirects, TLS, proxy headers, static and media serving, body size and timeouts, buffering, compression, security headers, logging, and safe reloads.
Problem statement
Nginx sits in front of nearly every self-hosted Django deployment, and most Django production issues that look like Django bugs (redirect loops, CSRF failures, wrong client IPs, 413s, 502s, missing admin CSS) are Nginx configuration gaps. This checklist covers the server block, TLS, the headers Django depends on, file serving, limits, and operations, so a configuration can be reviewed systematically instead of fixed one incident at a time.
- One server block per name and port; HTTP redirects to one canonical HTTPS host.
- TLS 1.2+ with a full chain; HSTS set once (Django or Nginx).
- Forward
Host,X-Forwarded-For, andX-Forwarded-Proto; Django trusts the proto header. - Serve
/static/(and public/media/) from disk with long cache headers for hashed files. - Set
client_max_body_sizeand proxy timeouts to match uploads and slow views. - Test with
nginx -tand reload, never restart blindly; log request time.
Checklist
Server blocks and routing
- Exactly one server block per name and port; no
conflicting server namewarnings. - A default server rejects unknown hosts (
return 444orssl_reject_handshake on). - HTTP redirects to HTTPS;
wwwand apex redirect to one canonical host. location /proxies to Gunicorn or Uvicorn; notry_files ... =404catching Django URLs.
TLS
fullchain.pemserved; certificate covers every name.ssl_protocols TLSv1.2 TLSv1.3;with modern ciphers; HTTP/2 enabled.- Certificate renewal tested (
certbot renew --dry-run) with a reload hook.
Proxy headers
proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;(or pass through the load balancer's value)- Django has
SECURE_PROXY_SSL_HEADERand correctALLOWED_HOSTSandCSRF_TRUSTED_ORIGINS. - Behind a CDN, real client IPs restored with
set_real_ip_fromandreal_ip_header.
Files
location /static/ { alias /srv/app/staticfiles/; }withexpires 1yorCache-Control: public, max-age=31536000, immutablefor hashed files.- Public media served with
alias; private media viainternallocations and X-Accel-Redirect. - Nginx can read the files (directory execute permissions, SELinux contexts).
Limits, buffering, compression
client_max_body_sizematches the largest legitimate upload.proxy_read_timeoutslightly above Gunicorn's timeout; longer only for streaming or WebSocket locations.proxy_buffering onby default; off only for streaming endpoints.- gzip (and Brotli if available) for text responses; not for already-compressed files.
- Optional rate limits on login and API locations.
Headers, logging, operations
- Security headers set once (Django's
SecurityMiddlewareor Nginx, not both). - Access logs include
$request_timeand$upstream_response_time; error log monitored. worker_connectionsand file limits sized;nginx -t && systemctl reload nginxin every deploy.
Explanation
Nginx terminates client connections, decides which server block and location handle a request, and forwards it to Django with whatever headers it is told to add. Django's security features (host validation, HTTPS detection, CSRF origin checks, client IP use) all depend on those headers. File serving, size limits, and buffering determine whether uploads succeed and whether Gunicorn workers are tied up by slow clients. Reviewing these areas together catches the configuration gaps behind most proxy-related incidents.
Edge cases / notes
- Containers: Nginx in a separate container needs the static files via a shared volume or its own image.
- Load balancers in front change which headers to trust; pass through their
X-Forwarded-Proto. - WebSockets need
UpgradeandConnectionheaders and long timeouts on their location. - Caddy sets most proxy headers by default; the same checklist applies conceptually.
Next steps
- Start from a complete config: Configure Nginx for Django in Production.
- Tune connections: Tune Nginx Keepalive and Worker Connections for Django.
FAQ
What headers must Nginx pass to Django?
Host, X-Forwarded-For, and X-Forwarded-Proto, with Django configured to trust the protocol header.
Should Nginx serve Django static files?
Yes on a VPS, with long cache headers for hashed files. WhiteNoise is an alternative when Nginx is not available.
What client_max_body_size should I use?
The largest legitimate upload plus margin; the default 1 MB is too small for most apps with uploads.
How do I safely apply Nginx changes?
Run nginx -t and then systemctl reload nginx, which keeps the old config if the new one is invalid.