Skip to content
ohsudo, Home
Networking

Nginx and Gunicorn for Django: production config, static files and HTTPS

Deploy Django with Gunicorn behind Nginx: a complete HTTPS config, a socket with systemd, static and media files, the settings you need behind a proxy, and common errors.

Intermediate3 min readApplies to Nginx 1.18+ · Django 4.2+ · Gunicorn · Python 3.10+ · Ubuntu · Debian

by OhSudo ·

Django’s runserver isn’t for production. The usual setup is Gunicorn to run your code, with Nginx in front for HTTPS and to serve static files directly. Your Python processes don’t waste time on images and CSS.

The Nginx Config Generator with these options writes the HTTPS and proxy parts. The /static/ and /media/ locations below are what you add.

The config

upstream django {
server unix:/run/gunicorn/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 20m;
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 30d;
access_log off;
}
location /media/ {
alias /var/www/myproject/media/;
}
location / {
proxy_pass http://django;
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;
}
}

1. Set up the project

Terminal window
cd /var/www/myproject
python3 -m venv .venv
.venv/bin/pip install django gunicorn

2. Settings for production

These are the ones that matter behind a proxy, in settings.py:

DEBUG = False
ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
# Trust Nginx's header, so Django knows the request was HTTPS.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

Set SECURE_PROXY_SSL_HEADER only behind a proxy that sets or removes that header, as this one does. Otherwise, a client could claim to be on HTTPS. Then collect the static files, and let Django check itself:

Terminal window
.venv/bin/python manage.py collectstatic --noinput
.venv/bin/python manage.py migrate
.venv/bin/python manage.py check --deploy

3. Run Gunicorn as a service

Save this as /etc/systemd/system/gunicorn.service:

[Unit]
Description=Gunicorn for Django
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myproject
RuntimeDirectory=gunicorn
ExecStart=/var/www/myproject/.venv/bin/gunicorn myproject.wsgi:application --workers 3 --bind unix:/run/gunicorn/gunicorn.sock
Restart=on-failure
[Install]
WantedBy=multi-user.target

RuntimeDirectory=gunicorn creates /run/gunicorn at each start. myproject is the folder that holds wsgi.py. A common starting point for workers is twice the CPU count plus one.

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn
sudo systemctl status gunicorn --no-pager
curl --unix-socket /run/gunicorn/gunicorn.sock http://localhost/ -H 'Host: example.com' -I

To apply a code change, restart it: sudo systemctl restart gunicorn. The systemd Unit Generator can write a fuller unit.

4. Get HTTPS, and turn it on

Terminal window
sudo certbot certonly --standalone -d example.com \
--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If it fails, see Certbot failed to authenticate.

When it goes wrong

400 Bad Request with DisallowedHost in the log. The name isn’t in ALLOWED_HOSTS, or proxy_set_header Host $host; is missing. Don’t use ["*"] in production.

403 CSRF verification failed on forms and the admin login. Since Django 4.0, HTTPS sites need CSRF_TRUSTED_ORIGINS, with the scheme: https://example.com. Check that X-Forwarded-Proto is being sent as well.

No styles, including on the admin. With DEBUG = False, Django stops serving static files, and Nginx has to. Run collectstatic, and check that alias points to STATIC_ROOT with a trailing /. A 403 on those files means Nginx can’t read that folder: see Nginx 403.

ERR_TOO_MANY_REDIRECTS. SECURE_SSL_REDIRECT = True with no SECURE_PROXY_SSL_HEADER: Django thinks each request is plain HTTP, and keeps redirecting. See Too many redirects.

502 Bad Gateway. Gunicorn isn’t running, or Nginx can’t reach the socket. The error log says which:

connect() to unix:/run/gunicorn/gunicorn.sock failed (2: No such file or directory)
connect() to unix:/run/gunicorn/gunicorn.sock failed (13: Permission denied)

The first means the service isn’t up, so see journalctl -u gunicorn -n 50. The second means a different user: use www-data as in the unit above. On RHEL, Nginx’s user is nginx, and SELinux needs sudo setsebool -P httpd_can_network_connect 1. More in Nginx 502.

504 or WORKER TIMEOUT in the Gunicorn log. A request took too long. Gunicorn’s default limit is 30 seconds, and Nginx’s is 60. Find the slow query before raising --timeout: see Nginx 504.

413 on uploads. Raise client_max_body_size: see Nginx 413.

Warning

Files in /media/ come from your users, so treat them as untrusted. If people can upload files, don’t let .html or .svg run on your main domain, as that allows script attacks on your logged-in users. Use a separate domain for them, or add add_header Content-Disposition attachment; to that location.

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles