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.
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.
On a Debian/Ubuntu VPS:
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"
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
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")
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.
If you don't already have a certificate, Certbot's Nginx plugin can obtain one and edit the config for you automatically:
Certbot also installs a systemd timer that renews the certificate automatically before it expires.
- 403 Forbidden on static/media files - usually a filesystem permissions issue. Nginx (running as
www-data) needs read access toSTATIC_ROOTand every parent directory leading up to it (chmod o+xon each parent, or addwww-datato 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_HEADERmakes Django think every request is plain HTTP, generatinghttp://links or redirecting HTTPS requests back to HTTPS in a loop. - Conflicting
server_namewarnings - if Certbot or a copy-pasted config leaves twoserverblocks 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 Largeunless you raiseclient_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.