How to Configure Nginx for Django

How to Configure Nginx for Django

Django’s built-in runserver command tells you explicitly, in its own startup message, not to use it in production. I appreciate that Django is upfront about it, but it still trips up a lot of people deploying their first app, because runserver genuinely works fine for casual testing — right up until real traffic hits it and things start falling apart. In this guide, I’ll walk through the setup I actually use: Nginx in front of Gunicorn, running a Django app, with static and media files handled properly (which is where I see most Django deployments go wrong).

Why Nginx and Gunicorn for Django?

Django, like Flask, is a WSGI application — it doesn’t manage its own production-grade HTTP server. Gunicorn fills that role, running your Django app as a pool of worker processes. Nginx sits in front of Gunicorn and handles everything Gunicorn shouldn’t have to worry about: TLS, static files, media uploads, and buffering slow clients.

Client → Nginx (80/443) → Gunicorn (Unix socket) → Django app

Requirements

  • A Linux server with Python 3 and a virtual environment set up for your Django project
  • Your Django project deployed (I’ll assume /var/www/mydjangoapp)
  • Nginx installed
  • Gunicorn installed in your virtual environment
  • STATIC_ROOT and MEDIA_ROOT configured in settings.py

Install Gunicorn:

cd /var/www/mydjangoapp
source venv/bin/activate
pip install gunicorn

Test Gunicorn manually before configuring Nginx:

gunicorn --bind 127.0.0.1:8000 mydjangoapp.wsgi:application

Replace mydjangoapp.wsgi with your actual project’s WSGI module path (it’s the folder containing settings.py, typically the same name as your project). Confirm it responds with curl http://127.0.0.1:8000.

Step 1: Configure Django Settings for Production

In settings.py, make sure these are set correctly before deploying:

DEBUG = False
ALLOWED_HOSTS = ['mydjangoapp.example.com']

STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/mydjangoapp/staticfiles'

MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/mydjangoapp/media'

DEBUG = False is critical for the same reason it matters in every other framework I’ve written about — leaving it on exposes stack traces, settings values, and internal paths to anyone who triggers an error.

Then collect static files into STATIC_ROOT:

python manage.py collectstatic --noinput

This gathers every app’s static assets (including Django admin’s CSS/JS) into one directory that Nginx can serve directly.

Step 2: Run Gunicorn as a Systemd Service

sudo nano /etc/systemd/system/mydjangoapp.service
[Unit]
Description=Gunicorn daemon for mydjangoapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/mydjangoapp
ExecStart=/var/www/mydjangoapp/venv/bin/gunicorn \
          --workers 3 \
          --bind unix:/var/www/mydjangoapp/mydjangoapp.sock \
          mydjangoapp.wsgi:application

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start mydjangoapp
sudo systemctl enable mydjangoapp
sudo systemctl status mydjangoapp

Step 3: Create the Nginx Server Block

sudo nano /etc/nginx/sites-available/mydjangoapp
server {
    listen 80;
    server_name mydjangoapp.example.com;

    access_log /var/log/nginx/mydjangoapp.access.log;
    error_log /var/log/nginx/mydjangoapp.error.log;

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

    location /static/ {
        alias /var/www/mydjangoapp/staticfiles/;
        expires 30d;
    }

    location /media/ {
        alias /var/www/mydjangoapp/media/;
        expires 7d;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/var/www/mydjangoapp/mydjangoapp.sock;
    }

    client_max_body_size 20M;
}

Notice the /static/ and /media/ locations point directly at the filesystem via alias — Nginx serves these without ever involving Gunicorn or Python. This is the single biggest performance win in a Django deployment, and it’s also the piece I most often see missing in tutorials, resulting in Django trying (and often failing, since runserver handles static files differently than production WSGI apps do) to serve its own static assets.

Step 4: Enable the Site

sudo ln -s /etc/nginx/sites-available/mydjangoapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Adding HTTPS

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d mydjangoapp.example.com

Once HTTPS is confirmed working, add these to settings.py for defense in depth:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

That last line is important and often missed — it tells Django to trust the X-Forwarded-Proto header Nginx sets, so Django correctly knows the original request was HTTPS even though the internal connection between Nginx and Gunicorn is plain HTTP.

Testing Your Setup

  1. sudo nginx -t — validate config
  2. sudo systemctl status mydjangoapp — confirm Gunicorn is running
  3. curl -I http://mydjangoapp.example.com
  4. Load a page with static assets (CSS should render correctly) and check the Django admin loads its styling properly — this is a quick way to confirm /static/ is wired up right
  5. Upload a file through your app (if applicable) and confirm it’s reachable under /media/
  6. Check sudo journalctl -u mydjangoapp -f alongside sudo tail -f /var/log/nginx/mydjangoapp.error.log while testing

Troubleshooting Common Issues

502 Bad Gateway — Gunicorn isn’t running, or the socket path doesn’t match between the systemd unit and the Nginx config. Check sudo systemctl status mydjangoapp and ls -l /var/www/mydjangoapp/mydjangoapp.sock.

Django admin loads with no CSS — Almost always means collectstatic was never run, or the /static/ alias path in Nginx doesn’t match STATIC_ROOT.

“DisallowedHost” errors — Your domain isn’t in ALLOWED_HOSTS in settings.py. Add it and restart Gunicorn.

Uploaded files 403/404ing after upload — Check that the /media/ alias path matches MEDIA_ROOT exactly, and that the Gunicorn process (running as www-data) has write permission to that directory.

Redirect loops after enabling SECURE_SSL_REDIRECT — This means Django isn’t correctly detecting HTTPS from the forwarded header. Double check SECURE_PROXY_SSL_HEADER is set exactly as shown, and that Nginx is actually sending X-Forwarded-Proto.

Security Considerations

  • DEBUG = False in production, always
  • Set ALLOWED_HOSTS explicitly — never leave it as ['*'] in production
  • Use SECURE_PROXY_SSL_HEADER correctly so Django’s security features (secure cookies, SSL redirect) work as intended behind Nginx
  • Keep SECRET_KEY out of source control — load it from an environment variable
  • Restrict the Gunicorn socket to be readable only by the Nginx user
  • Run python manage.py check --deploy before going live — it’s a built-in Django command that flags common production misconfigurations
  • Keep Django and all dependencies patched; check the Django security release page periodically

Performance Tips

  • Serve /static/ and /media/ directly from Nginx, never through Gunicorn — this is the single highest-impact change for a typical Django deployment
  • Tune Gunicorn workers using the (2 x CPU cores) + 1 formula as a starting point, then adjust based on load testing
  • Use gunicorn --worker-class gevent for I/O-bound Django apps that spend a lot of time waiting on external APIs or slow queries
  • Enable database connection pooling (e.g., via django-db-connection-pool or PgBouncer for Postgres) to avoid connection overhead on every request
  • Cache expensive views with Django’s cache framework, backed by Redis or Memcached
  • Enable gzip in Nginx for HTML/JSON/CSS/JS responses

Real-World Use Case

I’ve deployed this pattern for a content-heavy Django site with a large media library of user-uploaded images. Nginx handled all static and media file serving directly — completely bypassing Python for anything that wasn’t a dynamic page render — while Gunicorn ran four workers behind a Unix socket. Under load testing, this configuration handled several times the throughput of routing everything (including static files) through Gunicorn, simply because Nginx is so much more efficient at raw file serving than a Python WSGI worker.

Best Practices Recap

  • DEBUG = False, ALLOWED_HOSTS set correctly, always
  • Run collectstatic on every deploy before restarting Gunicorn
  • Serve /static/ and /media/ directly via Nginx alias directives
  • Set SECURE_PROXY_SSL_HEADER so Django correctly detects HTTPS
  • Use a Unix socket between Nginx and Gunicorn
  • Manage Gunicorn with systemd for reliability and auto-restart

The pattern that trips up almost every new Django deployment is static file handling — runserver handles it transparently in development, which creates a false sense that “it just works,” and then it doesn’t in production until you explicitly wire up collectstatic and the Nginx alias directives. Once you internalize that Django itself never serves static files in production, the rest of this setup is refreshingly predictable.

Total
1
Shares

Leave a Reply

Previous Post
How to Disable Directory Listing in Nginx

How to Disable Directory Listing in Nginx

Next Post
How to Set Up Nginx with Ruby on Rails

How to Set Up Nginx with Ruby on Rails

Related Posts