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.

Nginx in front of DjangoServer blocksnames, redirectsTLSmodern, HSTSProxy headersHost, proto, IPFiles + limitsstatic, body, timeoutsMost Django-in-production surprises trace back to one of these four areas.
  1. One server block per name and port; HTTP redirects to one canonical HTTPS host.
  2. TLS 1.2+ with a full chain; HSTS set once (Django or Nginx).
  3. Forward Host, X-Forwarded-For, and X-Forwarded-Proto; Django trusts the proto header.
  4. Serve /static/ (and public /media/) from disk with long cache headers for hashed files.
  5. Set client_max_body_size and proxy timeouts to match uploads and slow views.
  6. Test with nginx -t and reload, never restart blindly; log request time.

Checklist

Server blocks and routing

  • Exactly one server block per name and port; no conflicting server name warnings.
  • A default server rejects unknown hosts (return 444 or ssl_reject_handshake on).
  • HTTP redirects to HTTPS; www and apex redirect to one canonical host.
  • location / proxies to Gunicorn or Uvicorn; no try_files ... =404 catching Django URLs.

TLS

  • fullchain.pem served; 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_HEADER and correct ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.
  • Behind a CDN, real client IPs restored with set_real_ip_from and real_ip_header.
incident to missing settingIncidentMissing or wrong settingredirect loop on HTTPSX-Forwarded-Proto notset or not trustedDisallowedHost, wrong linksHost header not forwarded413 on uploadsclient_max_body_size too smalladmin CSS missingstatic location alias or permissionswrong client IPs in logsreal_ip module behind CDN or LBEach of these looks like a Django bug and is fixed in Nginxdjango-deployment.com
Review the proxy headers first whenever Django behaves differently behind Nginx.

Files

  • location /static/ { alias /srv/app/staticfiles/; } with expires 1y or Cache-Control: public, max-age=31536000, immutable for hashed files.
  • Public media served with alias; private media via internal locations and X-Accel-Redirect.
  • Nginx can read the files (directory execute permissions, SELinux contexts).

Limits, buffering, compression

  • client_max_body_size matches the largest legitimate upload.
  • proxy_read_timeout slightly above Gunicorn's timeout; longer only for streaming or WebSocket locations.
  • proxy_buffering on by 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 SecurityMiddleware or Nginx, not both).
  • Access logs include $request_time and $upstream_response_time; error log monitored.
  • worker_connections and file limits sized; nginx -t && systemctl reload nginx in every deploy.
before going liveOne block per name, canonical redirectdefault server rejects othersModern TLS, renewal testedfull chain, HTTP/2Host, X-Forwarded-For, -Proto setDjango trusts protoStatic and media served correctlylong cache for hashed filesBody size, timeouts, buffering setnginx -t before reloadA short list that prevents most proxy-related Django incidentsdjango-deployment.com
Keep the Nginx config in Git and deploy it like code.

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 Upgrade and Connection headers and long timeouts on their location.
  • Caddy sets most proxy headers by default; the same checklist applies conceptually.

Next steps

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.

Keep going