← Writing
cheatsheet intermediate

Nginx Configuration Patterns for Django

Production Nginx config reference for Django — reverse proxy setup, static file serving, WebSocket proxying, rate limiting, gzip, security headers, and multi-site patterns.

#nginx#django#deployment#websocket#ssl#performance#security

Nginx sits in front of every Django deployment I’ve run. This is the configuration reference I reach for — covering the common patterns from basic proxy setup to WebSocket, rate limiting, and security headers.

Minimal Django Reverse Proxy

upstream django_app {
    server unix:/run/gunicorn.sock fail_timeout=0;
    # Or TCP: server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://django_app;
        proxy_set_header Host $http_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;
    }
}

Why Unix socket over TCP port: The socket avoids the TCP stack entirely for local connections — lower latency and no port conflicts. Use TCP only if Nginx and Gunicorn are on different hosts.

Static Files

server {
    # ...

    location /static/ {
        alias /home/deploy/myproject/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location /media/ {
        alias /home/deploy/myproject/mediafiles/;
        expires 7d;
        add_header Cache-Control "public";
    }

    location = /favicon.ico {
        alias /home/deploy/myproject/staticfiles/favicon.ico;
        access_log off;
        log_not_found off;
    }
}

Nginx serves static files directly from disk — Django never sees these requests. immutable tells browsers the file won’t change as long as the URL is the same (safe because collectstatic hashes filenames).

HTTP → HTTPS Redirect

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;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # ... rest of config
}

WebSocket Proxying (Django Channels)

server {
    # ...

    # WebSocket connections must be explicitly proxied with Upgrade headers
    location /ws/ {
        proxy_pass http://django_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400;    # 24 hours — don't time out long-lived connections
        proxy_send_timeout 86400;
    }

    # Regular HTTP traffic
    location / {
        proxy_pass http://django_app;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
    }
}

The Upgrade and Connection headers are what transform an HTTP request into a WebSocket handshake. Without them, WebSocket connections fail silently from the client’s perspective.

Rate Limiting

# In http{} block (nginx.conf or conf.d/default.conf)
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

server {
    # ...

    # Rate-limit the API
    location /api/ {
        limit_req zone=api burst=10 nodelay;
        limit_req_status 429;
        proxy_pass http://django_app;
        # ...
    }

    # Strict limit on auth endpoints
    location /api/auth/login/ {
        limit_req zone=login burst=3 nodelay;
        limit_req_status 429;
        proxy_pass http://django_app;
        # ...
    }
}

burst=10 nodelay allows a burst of 10 requests to pass immediately, then enforces the rate. Without nodelay, bursts are queued and delayed rather than rejected. For login endpoints, you usually want strict rejection (nodelay).

Gzip Compression

# In http{} block
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
    application/json
    application/javascript
    application/xml
    text/css
    text/html
    text/javascript
    text/plain
    text/xml;

Don’t gzip images, videos, or already-compressed formats — it wastes CPU for no gain. gzip_min_length 256 skips tiny responses where the header overhead exceeds savings.

Security Headers

server {
    # ...

    # Prevent clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Prevent MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # XSS protection (legacy browsers)
    add_header X-XSS-Protection "1; mode=block" always;

    # Force HTTPS for 1 year, include subdomains
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Referrer policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Hide Nginx version from error pages
    server_tokens off;
}

always is important — without it, headers are only added on 2xx responses, not on errors like 404/500.

Client Upload Size

server {
    # ...

    # Default is 1MB — increase for file upload endpoints
    client_max_body_size 20M;

    # For specific upload paths only
    location /api/upload/ {
        client_max_body_size 100M;
        proxy_pass http://django_app;
        proxy_read_timeout 300;      # uploading takes longer
    }
}

Proxy Timeouts

server {
    # ...

    location / {
        proxy_pass http://django_app;

        proxy_connect_timeout 10s;   # time to establish connection to Gunicorn
        proxy_send_timeout    30s;   # time to send request to Gunicorn
        proxy_read_timeout    120s;  # time to receive response from Gunicorn

        # ...
    }
}

proxy_read_timeout is the one that matters most. If Django has a slow endpoint (report generation, bulk processing), increase this — otherwise Nginx closes the connection with a 504 Gateway Timeout.

Buffer Tuning

server {
    location / {
        proxy_pass http://django_app;

        proxy_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 8 16k;
        proxy_busy_buffers_size 32k;
    }
}

With buffering on, Nginx reads the full response from Gunicorn into memory before sending to the client. This frees Gunicorn workers faster — a worker handling a slow client would block with buffering off. For streaming responses (Server-Sent Events, large file downloads), set proxy_buffering off on those specific locations.

Multi-App on One Server

# App 1: main site
server {
    listen 443 ssl http2;
    server_name example.com;

    location / {
        proxy_pass http://unix:/run/gunicorn_main.sock;
        # ...
    }
}

# App 2: API subdomain
server {
    listen 443 ssl http2;
    server_name api.example.com;

    location / {
        proxy_pass http://unix:/run/gunicorn_api.sock;
        # ...
    }
}

# App 3: Admin behind basic auth
server {
    listen 443 ssl http2;
    server_name admin.example.com;

    location / {
        auth_basic "Admin Area";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_pass http://unix:/run/gunicorn_main.sock;
        # ...
    }
}

Each app gets its own Unix socket. Run htpasswd -c /etc/nginx/.htpasswd adminuser to create the password file.

Logging

http {
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    '$request_time';  # adds response time in seconds

    access_log /var/log/nginx/access.log main;
    error_log  /var/log/nginx/error.log warn;
}

server {
    # Per-server log
    access_log /var/log/nginx/mysite_access.log main;
    error_log  /var/log/nginx/mysite_error.log;

    # Disable logging for specific paths
    location /health/ {
        access_log off;
        return 200 "ok";
    }
}

Health Check Endpoint

server {
    # ...

    # Nginx-level health check (doesn't hit Django)
    location = /nginx-health {
        access_log off;
        return 200 "ok\n";
        add_header Content-Type text/plain;
    }

    # Django-level health check
    location = /health/ {
        proxy_pass http://django_app;
        access_log off;
    }
}

Load balancers ping the health endpoint. Responding at Nginx level avoids warming up Django for health checks. Use the Django-level endpoint when you want to verify DB connectivity too.

Useful Commands

# Test config for syntax errors
sudo nginx -t

# Reload config without dropping connections
sudo systemctl reload nginx

# Full restart (drops active connections)
sudo systemctl restart nginx

# View access log live
sudo tail -f /var/log/nginx/access.log

# View error log
sudo tail -f /var/log/nginx/error.log

# Check which config file is loaded
sudo nginx -T | head -20