Deployment
#django
#nginx

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:8000 or 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, and X-Forwarded-Proto
  • test with nginx -t before 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=False
  • ALLOWED_HOSTS contains 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 alias for /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_name matches 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 Host header 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 collectstatic output is broken or missing files, restore the previous static directory or re-run the release process correctly

A practical recovery sequence is:

  1. confirm whether the failure is Nginx, app server, socket permission, or static-file related
  2. restore the last known-good component
  3. test locally against the socket or 127.0.0.1:8000
  4. 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_size intentionally. 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.

For background, see What Nginx Does in a Django Production Stack.

Related implementation guides:

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_HEADER
  • CSRF_TRUSTED_ORIGINS
  • ALLOWED_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.

2026 · django-deployment.com - Django Deployment knowledge base