Deploying With Gunicorn, Nginx, and systemd

Introduction

python manage.py runserver is for local development only—it is single-threaded, not hardened, and must never face the public internet. Production Django runs behind Gunicorn (WSGI workers), Nginx (reverse proxy, TLS, static files), and systemd (process supervision). This chapter deploys hello-django on Linux using environment-based settings from Settings and Multi-Environment.

Prerequisites

Development vs Production

runserverGunicorn + Nginx
PurposeLocal devProduction
Auto reloadYesRedeploy to update
Static filesDev helperNginx or WhiteNoise
SecurityDebug pagesDEBUG=False

Warning

Never Expose runserver Publicly

Use Gunicorn (or Uvicorn for ASGI) bound to localhost behind Nginx.

Install Gunicorn

bash
pip install gunicorn

Add to requirements.txt.

Run Gunicorn Manually

From project root (folder with manage.py):

bash
export DJANGO_SETTINGS_MODULE=config.settings.production
export DJANGO_SECRET_KEY="your-production-secret"
export DJANGO_ALLOWED_HOSTS="example.com,www.example.com"
 
python manage.py migrate
python manage.py collectstatic --noinput
 
gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000

Code explanation:

  • config.wsgi:application — WSGI callable from project structure
  • -w 4 — worker processes (start with 2 × CPU + 1)
  • -b 127.0.0.1:8000 — local only; Nginx is public-facing

Unix socket (optional):

bash
gunicorn config.wsgi:application -w 4 --bind unix:/run/hello-django.sock

Nginx proxy_pass http://unix:/run/hello-django.sock.

Worker Tuning

OptionMeaning
-w 4Worker count
--timeout 120Kill hung workers (seconds)
--access-logfile -Access log to stdout (Docker/systemd)
--error-logfile -Error log to stdout
--max-requests 1000Recycle workers (memory leak mitigation)

Align --timeout with Nginx proxy_read_timeout for slow uploads.

Health Check Route

blog/views.py:

python
from django.http import JsonResponse
 
 
def health(request):
    return JsonResponse({"status": "ok"})

blog/urls.py:

python
path("health/", views.health, name="health"),

Load balancers probe GET /health/—keep it lightweight (optional DB ping for deep checks).

Environment File (12-Factor)

/etc/hello-django/env (root-only, chmod 600):

env
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=long-random-string
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DB_NAME=hello_django
DB_USER=django_user
DB_PASSWORD=strong-password
DB_HOST=127.0.0.1
DB_PORT=3306

Never commit this file—mirror structure in .env.example without secrets for developers.

systemd Unit

/etc/systemd/system/hello-django.service:

Enable:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now hello-django
sudo systemctl status hello-django
journalctl -u hello-django -f

Deploy updates:

bash
cd /var/www/hello-django
git pull
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart hello-django

Nginx Configuration

/etc/nginx/sites-available/hello-django:

Enable and test:

bash
sudo ln -s /etc/nginx/sites-available/hello-django /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

See Nginx reverse proxy, Tomcat + Nginx pattern (same proxy_pass idea), and proxying Spring Boot.

HTTPS: Let's Encrypt.

ASGI Alternative (Awareness)

For async views or Django Channels:

bash
pip install uvicorn
uvicorn config.asgi:application --host 127.0.0.1 --port 8000

This track’s sync apps use WSGI + Gunicorn by default.

File Permissions

bash
sudo chown -R www-data:www-data /var/www/hello-django
sudo chmod 750 /var/www/hello-django
sudo mkdir -p /var/www/hello-django/media /var/www/hello-django/logs

Gunicorn runs as www-data; Nginx reads static; Django writes uploads to media/.

Pre-Flight Checklist

bash
DJANGO_SETTINGS_MODULE=config.settings.production python manage.py check --deploy
curl -I http://127.0.0.1:8000/health/
curl -I http://example.com/static/blog/css/style.css

Common Problems

SymptomFix
502 Bad GatewayGunicorn down; check systemctl status, socket/port
DisallowedHostAdd domain to ALLOWED_HOSTS
Static 404Run collectstatic; verify Nginx alias path
Media 404Create media/; Nginx location /media/
502 after long requestIncrease Gunicorn/Nginx timeouts
Permission denied on socketwww-data ownership on /run/ socket

Post-Chapter Checklist

  • Gunicorn serves config.wsgi:application on localhost
  • systemd unit starts on boot and restarts on failure
  • Nginx proxies / and serves /static/, /media/
  • migrate and collectstatic in deploy script
  • check --deploy passes with production settings

FAQ## FAQ

Gunicorn on Windows?

Gunicorn is Unix-focused; use WSL/Linux VPS or Waitress for Windows experiments.

How many workers?

Start (2 × CPU cores) + 1; profile memory per worker—Django workers are heavier than Node.

Zero-downtime deploy?

Blue/green or rolling with two versions + load balancer—advanced; restart causes brief 502 on single node.

WhiteNoise without Nginx static?

Possible for small sites—Nginx still recommended for TLS and media.

Multiple Django sites on one server?

Separate systemd units, ports/sockets, and Nginx server_name blocks.