How to Configure Nginx for Django in Production
A production Django stack should not expose Gunicorn or Uvicorn directly to the internet.
Problem statement
A production Django stack should not expose Gunicorn or Uvicorn directly to the internet. You need a reverse proxy in front of the app server to handle TLS, serve static files efficiently, forward requests safely, and give you a stable place to apply request limits and logging.
The real problem is not just “make Nginx work.” It is configuring Nginx so Django behaves correctly in production:
- requests reach the app server reliably
- static files are served directly by Nginx
- HTTPS is enforced
- Django sees the correct host and scheme, and receives client IP headers from Nginx
- file permissions and socket access do not break requests
- you can test and roll back changes safely
A bad Nginx configuration often causes 502 errors, broken static assets, CSRF failures, insecure cookies, or a site that reloads cleanly but fails under real traffic.
Quick answer
For a safe Django production Nginx setup, use this pattern:
- run Django with Gunicorn or Uvicorn bound to
127.0.0.1:8000or a Unix socket - put Nginx in front as the public entry point
- let Nginx serve
/static/directly - serve
/media/from Nginx only if media files are stored on the same server - terminate TLS at Nginx
- pass
Host,X-Real-IP,X-Forwarded-For, andX-Forwarded-Proto - test with
nginx -tbefore reload - verify Django settings such as
ALLOWED_HOSTS,CSRF_TRUSTED_ORIGINS, and secure proxy handling
Step-by-step solution
Step 1 — Confirm the Django app server is working first
Before putting Nginx in front, make sure Gunicorn or Uvicorn already works locally.
If using Gunicorn via systemd:
sudo systemctl status gunicorn
sudo journalctl -u gunicorn -n 50 --no-pager
If using TCP on port 8000:
ss -ltnp | grep 8000
curl http://127.0.0.1:8000/
If using a Unix socket:
ls -l /run/gunicorn.sock
curl --unix-socket /run/gunicorn.sock http://localhost/
Also confirm Django production settings match the domain:
ALLOWED_HOSTS = ["example.com", "www.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Verification checks:
- the app responds directly on localhost or socket
DEBUG=FalseALLOWED_HOSTScontains the production domain- CSRF trusted origins include the HTTPS URL
If this step fails, fix the app server first. Nginx will not fix an unhealthy Django process.
Step 2 — Create the Nginx server block for Django
Choose Unix socket or TCP upstream:
- use a Unix socket when Nginx and Gunicorn run on the same host
- use TCP when the app server runs in a container or on another host
Example site config for Gunicorn over a Unix socket:
upstream django_app {
server unix:/run/gunicorn.sock;
}
server {
listen 80;
server_name example.com www.example.com;
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log;
client_max_body_size 10M;
server_tokens off;
location /static/ {
alias /srv/myapp/staticfiles/;
access_log off;
expires 7d;
add_header Cache-Control "public";
}
location /media/ {
alias /srv/myapp/media/;
}
location / {
proxy_pass http://django_app;
proxy_http_version 1.1;
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
If using TCP instead:
upstream django_app {
server 127.0.0.1:8000;
}
Notes:
- use
aliasfor/static/and/media/when mapping URL paths to filesystem directories - the trailing slash matters:
location /static/ { alias /path/to/staticfiles/; } - do not point Nginx at your project source tree unless that is actually where collected files live
Verification checks:
server_namematches your real domain- static and media paths exist on disk
- socket path matches the Gunicorn service config
Rollback note: keep a backup before editing.
sudo cp /etc/nginx/sites-available/myapp /etc/nginx/sites-available/myapp.bak
Step 3 — Serve static and media files correctly
Collect static files before expecting Nginx to serve them:
python manage.py collectstatic --noinput
Check the target directory:
ls -lah /srv/myapp/staticfiles/
If media is stored locally, verify that directory too:
ls -lah /srv/myapp/media/
Nginx must be able to read those files, and if using a socket, it must be able to connect to it. Depending on your distro and service user, that may require adjusting ownership or group membership carefully.
A common pattern is:
- Gunicorn writes the socket under
/run/ - Nginx runs as
www-data - the socket group allows Nginx access
Useful checks:
ps aux | egrep 'nginx: worker|nginx: master'
namei -l /run/gunicorn.sock
Do not use Django to serve static files in normal production traffic. Let Nginx handle them directly.
Verification checks:
curl -I http://example.com/static/admin/css/base.css
You want a 200 OK from Nginx, not a redirect to Django.
Step 4 — Enable HTTPS and secure the Nginx configuration
Add an HTTP-to-HTTPS redirect and define a complete TLS site config.
Example with a Unix socket upstream:
upstream django_app {
server unix:/run/gunicorn.sock;
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log;
client_max_body_size 10M;
server_tokens off;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location /static/ {
alias /srv/myapp/staticfiles/;
access_log off;
expires 7d;
add_header Cache-Control "public";
}
location /media/ {
alias /srv/myapp/media/;
}
location / {
proxy_pass http://django_app;
proxy_http_version 1.1;
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
If using TCP instead:
upstream django_app {
server 127.0.0.1:8000;
}
In Django, make sure secure requests are recognized:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
Use SECURE_SSL_REDIRECT = True only when your proxy setup is correct and all public traffic should be HTTPS.
Be cautious with HSTS. Only enable it after HTTPS works consistently across the site and subdomains.
Verification checks:
curl -I http://example.com/
curl -I https://example.com/
curl -I -H 'Host: example.com' http://127.0.0.1:8000/
You should see HTTP redirect to HTTPS, and HTTPS should return a valid response. If secure cookies or CSRF behavior is inconsistent, verify in Django that requests behind Nginx are treated as HTTPS.
Step 5 — Enable the site and test safely
Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
If the default site is enabled and may conflict, disable it:
sudo rm -f /etc/nginx/sites-enabled/default
Test configuration before reload:
sudo nginx -t
If valid, reload without dropping active connections:
sudo systemctl reload nginx
If the test fails, do not reload. Fix the syntax first.
Verification checks:
curl -I https://example.com/
curl -I https://example.com/static/admin/css/base.css
tail -f /var/log/nginx/example.error.log
sudo journalctl -u gunicorn -f
Important: a successful Nginx reload does not prove the Django app is healthy. The upstream process, socket target, collected static files, or the latest app release can still be broken.
Step 6 — Verify end-to-end production behavior
Check:
- homepage loads
- Django admin loads
- static files return
200 - form submissions work
- login and CSRF-protected views work over HTTPS
If secure cookies or CSRF fail behind the proxy, the usual causes are:
- missing
X-Forwarded-Proto - missing
SECURE_PROXY_SSL_HEADER - incorrect
CSRF_TRUSTED_ORIGINS - wrong
Hostheader handling
To inspect real behavior, use the logs:
tail -f /var/log/nginx/example.access.log
tail -f /var/log/nginx/example.error.log
sudo journalctl -u gunicorn -f
Rollback and recovery notes:
- if the Nginx config is wrong, restore the previous site file, test with
nginx -t, then reload - if the app release is bad, rolling back Nginx alone will not help; you may need to restore the previous Gunicorn service target, previous app code, or previous static files
- if the socket path changed, fix the app server unit or Nginx upstream so they match again
- if a new
collectstaticoutput is broken or missing files, restore the previous static directory or re-run the release process correctly
A practical recovery sequence is:
- confirm whether the failure is Nginx, app server, socket permission, or static-file related
- restore the last known-good component
- test locally against the socket or
127.0.0.1:8000 - only then reload Nginx and verify through HTTPS
Explanation
This setup works because Nginx handles the network-facing responsibilities while Gunicorn or Uvicorn runs the Django application process.
Nginx should serve static assets because it is more efficient than routing every CSS, JS, and image request through Django. That reduces app worker load and improves latency.
Proxy headers matter because Django uses them for security decisions. Without Host and X-Forwarded-Proto, Django may mis-detect the request origin or scheme, leading to bad redirects, cookie issues, or CSRF errors.
Unix sockets are usually better than localhost TCP when both services run on the same machine. They are simple and avoid exposing an extra local port. TCP is more flexible for containers, separate hosts, or debugging with common networking tools.
The X-Real-IP and X-Forwarded-For headers are enough for a direct Nginx-to-Django setup on one server. If Nginx is itself behind a load balancer or another proxy, client IP handling needs additional real_ip configuration in Nginx. Do not assume forwarded client IPs are trustworthy unless your proxy chain is configured explicitly.
If uploads, media storage, or traffic volume grows, split responsibilities. For example:
- keep Nginx as reverse proxy
- move media to object storage
- place a load balancer in front of multiple app instances
When to automate this
Once you manage more than one environment or repeat this process often, convert it into a reusable script or template. The most useful parts to automate first are config generation, backup of the previous server block, nginx -t validation, safe reload, and basic smoke tests for /, /admin/, and a static asset URL.
Edge cases / notes
- Multiple Django sites on one server: create one server block per domain and keep static paths separate.
- Dockerized Django behind host Nginx: TCP upstream is usually easier than a host-level Unix socket.
- Large uploads: increase
client_max_body_sizeintentionally. Match it to app requirements. - Long-running requests: adjust
proxy_read_timeout, but do not hide slow views with very large timeouts unless you understand the cause. - WebSockets with ASGI: add upgrade headers if you use Channels or other WebSocket features. A plain WSGI setup does not need them.
- 502/504 protection: most 502s come from a dead app process, wrong socket path, or permission problems. Most 504s come from upstream timeouts or stalled app code.
- Local media exposure: serving
/media/directly from Nginx is fine for public files stored on the same server. If user uploads require access control, do not expose them blindly with a simple public alias. - Secret management: Nginx configuration does not replace Django secret handling. Keep
SECRET_KEY, database credentials, and API tokens out of the Nginx config and manage them separately.
Internal links
For background, see What Nginx Does in a Django Production Stack.
Related implementation guides:
- Deploy Django with Gunicorn and Nginx on Ubuntu
- How to Serve Django Static Files in Production
- Deploy Django ASGI with Uvicorn and Nginx
For troubleshooting, see How to Fix 502 Bad Gateway Between Nginx and Gunicorn.
FAQ
Should Nginx proxy to Gunicorn over a Unix socket or localhost TCP?
Use a Unix socket when Nginx and Gunicorn are on the same host. Use localhost TCP when the app runs in Docker, on another host, or when you want simpler network debugging.
How do I configure Nginx to serve Django static files in production?
Collect static files into a dedicated directory, then map /static/ to that directory with alias. Example:
location /static/ {
alias /srv/myapp/staticfiles/;
}
Do not rely on Django to serve static files in normal production traffic.
What proxy headers does Django need behind Nginx?
At minimum:
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;
And in Django:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Why does Django show CSRF or insecure request issues behind Nginx?
Usually because Django does not know the original request was HTTPS, or the origin is not trusted. Check:
proxy_set_header X-Forwarded-Proto $scheme;SECURE_PROXY_SSL_HEADERCSRF_TRUSTED_ORIGINSALLOWED_HOSTS
How do I roll back a broken Nginx config without taking the site offline?
Keep a backup of the previous file, restore it, test, then reload:
sudo cp /etc/nginx/sites-available/myapp.bak /etc/nginx/sites-available/myapp
sudo nginx -t && sudo systemctl reload nginx
If the new site config should be disabled entirely:
sudo rm /etc/nginx/sites-enabled/myapp
sudo nginx -t && sudo systemctl reload nginx
If the failure is really in the Django app, socket target, or static files, roll back that part of the release too. Nginx config rollback only fixes Nginx-level mistakes.