Technology 10 min read

Django Deployment: From manage.py runserver to Production

Replace the dev server with Gunicorn, Nginx, PostgreSQL, and WhiteNoise for a production-grade Django deployment.

A admin Sep 06, 2026 44 views Updated Sep 07, 2026
Django Deployment: From manage.py runserver to Production

From Development to Production

When you're building a Django application, python manage.py runserver is your best friend during development. But the moment you need to go live, that same command becomes your biggest risk. The Django development server is single-threaded, has no process management, and explicitly warns you not to use it in production.

This guide walks you through replacing runserver with a production-grade stack: Gunicorn, Nginx, PostgreSQL, and WhiteNoise.

The Production Stack

Django production deployment has three standard layers:

  1. Gunicorn — WSGI application server that runs Django across multiple worker processes.
  2. Nginx — Reverse proxy that handles TLS, serves static files, and buffers slow clients.
  3. PostgreSQL — Production database replacing SQLite.

Step 1: Production Settings

Create a separate settings file for production. Here's what you need:

DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = ["yourdomain.com", "www.yourdomain.com"]

# Database
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("DB_NAME", "modernblog"),
        "USER": os.environ.get("DB_USER"),
        "PASSWORD": os.environ.get("DB_PASSWORD"),
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
    }
}

# Security
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
X_FRAME_OPTIONS = "DENY"

Step 2: Install Gunicorn

pip install gunicorn

Test it locally:

gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3

Step 3: Serve Static Files with WhiteNoise

WhiteNoise serves static files directly from Gunicorn with aggressive caching. No extra infrastructure needed.

pip install whitenoise

Add to your middleware and settings:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... other middleware
]

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

Collect static files:

python manage.py collectstatic --no-input

Step 4: Configure Nginx

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location /static/ {
        alias /opt/www/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

Step 5: Systemd Service

Create /etc/systemd/system/gunicorn.service:

[Unit]
Description=Gunicorn daemon for Django
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/www
ExecStart=/opt/www/.venv/bin/gunicorn config.wsgi:application     --bind 127.0.0.1:8000     --workers 3     --access-logfile /var/log/gunicorn/access.log     --error-logfile /var/log/gunicorn/error.log

[Install]
WantedBy=multi-user.target

Start the services:

sudo systemctl daemon-reload
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
sudo systemctl restart nginx

Step 6: Run Migrations

Always run migrations before reloading Gunicorn — never after:

python manage.py migrate --no-input

Deployment Checklist

# Before every production deployment:
python manage.py check --deploy
python manage.py migrate --plan
python manage.py test --no-input
python manage.py collectstatic --no-input
sudo systemctl reload gunicorn

Why Not Just Use runserver?

FeaturerunserverGunicorn + Nginx
Thread safetySingle-threadedMulti-worker
Static filesUncompressedCompressed + cached
Security auditsNoneProduction-hardened
Process managementNoneSystemd auto-restart
SSL/TLSNoYes (via Nginx)
Zero-downtime reloadNoYes (systemctl reload)

Conclusion

Moving from manage.py runserver to a Gunicorn + Nginx + PostgreSQL stack is the single most important step in Django deployment. It gives you security, performance, and reliability. The setup takes about 30 minutes, and the peace of mind is worth every second.

Comments (0)

Join the discussion — sign in to comment.

Sign in

No comments yet — be the first to share your thoughts.

Related reading