Django Deployment 2026-09-01

Configuring Nginx to Serve Django Applications

Nginx is the de-facto standard web server sitting in front of almost every production Django deployment. Django's built-in application servers (whether WSGI or ASGI) are excellent at running Python code, but they are not designed to be exposed directly to the internet: they don't efficiently serve static files, don't buffer slow clients, and don't handle TLS termination. Nginx fills exactly that gap - it terminates HTTP/HTTPS connections, serves static and media files directly from disk, and forwards everything else to your Django application server over a fast local socket or port.

In this post we'll cover the theory behind this setup, look at a request-flow diagram, and walk through a complete, production-ready Nginx configuration for a Django project.

Why Put Nginx in Front of Django?

  • Static & media file serving - Nginx reads files from disk and streams them directly, which is dramatically faster than routing every CSS/JS/image request through Python.
  • Reverse proxying - Nginx forwards dynamic requests to Gunicorn/uWSGI/Uvicorn, keeping your application server isolated from the public internet.
  • TLS termination - Nginx handles HTTPS certificates (e.g. via Let's Encrypt/Certbot), so Django itself never has to deal with SSL.
  • Buffering & protection - Nginx buffers slow or malicious clients, protecting your (typically much more expensive) application workers from being tied up by slow connections.
  • Load balancing - a single Nginx instance can distribute traffic across multiple application server processes or even multiple machines.
How the Pieces Fit Together

The typical request lifecycle looks like this:

flowchart LR
    A[Browser
HTTPS request] -->|":443"| B[Nginx] B -->|"/static/*, /media/*"| C[(Filesystem
disk I/O)] B -->|proxy_pass| D[Gunicorn
socket] D -->|WSGI/ASGI call| E[Django application]

Nginx inspects the request path first. If it matches /static/ or /media/, Nginx serves the file itself and Django is never involved. For everything else, Nginx proxies the request to the application server (Gunicorn, uWSGI, or Uvicorn), which in turn calls into your Django code, generates a response, and hands it back to Nginx, which forwards it to the browser.

1. Install Nginx

On a Debian/Ubuntu VPS:

sudo apt update && sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl status nginx ● nginx.service - A high performance web server Active: active (running)
2. Collect Django's Static Files

Nginx needs a real directory on disk to serve static assets from. Run Django's collectstatic management command, which copies every app's static files into STATIC_ROOT:


# core/settings.py
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
              
python manage.py collectstatic --noinput 128 static files copied to '/home/ubuntu/PROJECTS/scientific_dev/staticfiles'.
3. Configure the Application Server Socket

Whether you're using Gunicorn, Uvicorn, or uWSGI, the application server should bind to a Unix socket rather than a TCP port whenever Nginx and Django live on the same machine - it's faster and avoids exposing an extra port. See our post on migrating from uWSGI to Gunicorn + Uvicorn for a full systemd service example. The key point for Nginx is simply the socket path, e.g.:


/home/ubuntu/PROJECTS/scientific_dev/gunicorn.sock
              
4. Write the Nginx Server Block

Create /etc/nginx/sites-available/scientific_dev:


server {
    listen 80;
    listen [::]:80;
    server_name scientific-dev.example.com;

    # Redirect all HTTP traffic to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name scientific-dev.example.com;

    client_max_body_size 20M;

    # Static files - served directly by Nginx, never touches Django
    location /static/ {
        alias /home/ubuntu/PROJECTS/scientific_dev/staticfiles/;
        expires 30d;
        add_header Cache-Control "public";
    }

    # Media (user-uploaded) files
    location /media/ {
        alias /home/ubuntu/PROJECTS/scientific_dev/media/;
        expires 7d;
    }

    location = /favicon.ico { access_log off; log_not_found off; }

    # Everything else goes to the Django application server
    location / {
        include proxy_params;
        proxy_pass http://unix:/home/ubuntu/PROJECTS/scientific_dev/gunicorn.sock;
    }

    ssl_certificate     /etc/letsencrypt/live/scientific-dev.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/scientific-dev.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
              

The proxy_params file (usually shipped at /etc/nginx/proxy_params on Debian/Ubuntu) sets the headers Django needs to correctly build absolute URLs and know the client's real IP and protocol:


# /etc/nginx/proxy_params
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;
              

Important: without X-Forwarded-Proto, Django won't know the original request was HTTPS. Make sure your settings honor it:


# core/settings.py
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
              
5. Enable the Site and Test the Configuration
sudo ln -s /etc/nginx/sites-available/scientific_dev /etc/nginx/sites-enabled/ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful sudo systemctl reload nginx

Always run nginx -t before reloading - a broken config file will otherwise bring the whole web server down on reload, taking every site on the VPS with it.

6. Obtain a TLS Certificate with Certbot

If you don't already have a certificate, Certbot's Nginx plugin can obtain one and edit the config for you automatically:

sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d scientific-dev.example.com Successfully received certificate. Congratulations! You have successfully enabled HTTPS

Certbot also installs a systemd timer that renews the certificate automatically before it expires.

Common Pitfalls
  • 403 Forbidden on static/media files - usually a filesystem permissions issue. Nginx (running as www-data) needs read access to STATIC_ROOT and every parent directory leading up to it (chmod o+x on each parent, or add www-data to the owning group).
  • 502 Bad Gateway - Nginx can't reach the application server. Check that Gunicorn/uWSGI is running and that the socket path in the Nginx config exactly matches the one the service binds to.
  • Mixed content / infinite redirect loops - forgetting SECURE_PROXY_SSL_HEADER makes Django think every request is plain HTTP, generating http:// links or redirecting HTTPS requests back to HTTPS in a loop.
  • Conflicting server_name warnings - if Certbot or a copy-pasted config leaves two server blocks listening on the same port with the same name, Nginx will warn and silently ignore the duplicate. See our troubleshooting notes for a real-world example of this exact problem.
  • Request body too large - file uploads bigger than the default 1MB limit are rejected with 413 Request Entity Too Large unless you raise client_max_body_size.

Conclusion

Nginx and Django complement each other perfectly: Nginx excels at everything related to the network and the filesystem (TLS, static files, buffering, load balancing), while Django and its application server focus purely on executing your Python business logic. Once the static/media locations and the proxy block are wired up correctly, the two layers rarely need to be touched again - most future changes will be limited to adding new domains or tweaking cache headers.


Scientific Dev
Scientific Dev
Educator & Developer