How to Deploy Django on a VPS with Gunicorn, Nginx and a Unix Socket
I deploy a lot of Django apps to plain Linux VPS boxes, and over time I’ve settled on one layout I use every single time. It’s boring, repeatable, and survives reboots — which is exactly what you want in production. Here’s the whole thing.
The wrapper layout
I never put the virtualenv inside the repo, and I never bind gunicorn to a TCP port. Instead, each app gets a wrapper directory holding three siblings:
myproject/
├── run/ # gunicorn.sock lives here
├── .venv/ # virtualenv — sibling of the code, never inside it
└── app/ # the repo: manage.py, settings, .env
Keeping .venv outside the code means I can nuke and rebuild the environment without touching a single tracked file. (Moving a venv breaks it — the shebangs in .venv/bin/* are absolute paths — so always recreate it at the new location, never mv it.)
Gunicorn over a unix socket
For an app and nginx on the same box, a unix socket beats a TCP port: no port bookkeeping, and it’s not exposed to the network. The systemd unit:
[Unit]
Description=myproject (Django/gunicorn)
After=network.target
[Service]
User=root
Group=www-data
WorkingDirectory=/home/ubuntu/myproject/app
EnvironmentFile=/home/ubuntu/myproject/app/.env
ExecStart=/home/ubuntu/myproject/.venv/bin/gunicorn config.wsgi \
--bind unix:/home/ubuntu/myproject/run/gunicorn.sock --umask 007 --workers 3 --timeout 60
Restart=always
[Install]
WantedBy=multi-user.target
Group=www-data plus --umask 007 makes the socket 0770 root:www-data, so nginx (running as www-data) can connect to it but nothing else can.
Nginx in front
server {
listen 443 ssl;
server_name example.com www.example.com;
location /static/ { alias /home/ubuntu/myproject/app/staticfiles/; expires 30d; }
location /media/ { alias /home/ubuntu/myproject/app/media/; expires 30d; }
location / {
proxy_pass http://unix:/home/ubuntu/myproject/run/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
If you’re behind Cloudflare, set SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') in settings so Django knows the original request was HTTPS — otherwise CSRF and the admin will fight you.
The update flow
- Push code to the box (git pull or rsync).
.venv/bin/pip install -r requirements.txtmanage.py migratemanage.py collectstatic --noinputsystemctl restart myproject
Gotchas I’ve hit
- 502 right after switching to the socket — that’s the graceful-reload drain window; it clears once the reload finishes. Re-test after a second.
- Permission denied on the socket — the parent directories must be traversable by
www-data(o+x). - HTTPS 403 on forms — missing
CSRF_TRUSTED_ORIGINSor the proxy SSL header.
FAQ
Should I use a unix socket or a TCP port? For nginx + gunicorn on the same machine, a socket — it’s faster and not network-exposed. Use TCP only when the proxy is on a different host.
Where should the virtualenv live? Outside the repo, beside it. That keeps deploys clean and the env disposable.
Do I need certbot behind Cloudflare? No — if your DNS is proxied, use a Cloudflare Origin cert (or a self-signed cert with SSL mode Full). HTTP-01 challenges break on proxied DNS.