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.
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
cd /var/www/myprojectpython3 -m venv .venv.venv/bin/pip install django gunicorn2. Settings for production
These are the ones that matter behind a proxy, in settings.py:
DEBUG = FalseALLOWED_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 = TrueCSRF_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:
.venv/bin/python manage.py collectstatic --noinput.venv/bin/python manage.py migrate.venv/bin/python manage.py check --deploy3. Run Gunicorn as a service
Save this as /etc/systemd/system/gunicorn.service:
[Unit]Description=Gunicorn for DjangoAfter=network.target
[Service]User=www-dataGroup=www-dataWorkingDirectory=/var/www/myprojectRuntimeDirectory=gunicornExecStart=/var/www/myproject/.venv/bin/gunicorn myproject.wsgi:application --workers 3 --bind unix:/run/gunicorn/gunicorn.sockRestart=on-failure
[Install]WantedBy=multi-user.targetRuntimeDirectory=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.
sudo systemctl daemon-reloadsudo systemctl enable --now gunicornsudo systemctl status gunicorn --no-pagercurl --unix-socket /run/gunicorn/gunicorn.sock http://localhost/ -H 'Host: example.com' -ITo 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
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 -tsudo systemctl reload nginxIf 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.