Deploy Django to Production: A Practical Guide

Khimananda Oli 7 min read Programming and Languages
Deploy Django to Production: A Practical Guide

By Khimananda Oli | Last reviewed: August 2026

Most Django tutorials stop at runserver, leaving teams unprepared when traffic hits or audits begin. To successfully deploy Django to production, you must replace the development server with an application server like Gunicorn behind a reverse proxy such as Nginx, managed by systemd for resilience. This guide walks through the exact configuration I use for client projects, ensuring your stack is secure, observable, and performant from day one.

ClientNginxStatic FilesTLS / HeadersRate LimitingGunicornWSGI WorkersUnix SocketDjango AppPostgreSQLRedis Cache
Production architecture for deploying Django to production with Nginx, Gunicorn, and backend services

How do you configure Gunicorn to deploy Django to production reliably?

Gunicorn is the de facto standard WSGI server for Django in 2026. It handles concurrency, worker recycling, and graceful restarts—none of which manage.py runserver supports. The key is binding to a Unix socket rather than a TCP port when sitting behind Nginx on the same host; this avoids TCP overhead and restricts access to the local filesystem.

Create a dedicated Gunicorn config file

Avoid long CLI flags in systemd units. Instead, create /etc/gunicorn/myproject.py:

<!-- /etc/gunicorn/myproject.py -->
bind = "unix:/run/gunicorn/myproject.sock"
workers = 4  # Start with (2 × CPU cores) + 1
worker_class = "gthread"
threads = 2
max_requests = 1000
max_requests_jitter = 50
timeout = 120
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"
  • Workers: For CPU-bound Django views, use sync workers. For I/O-heavy workloads (database queries, API calls), gthread or gevent prevents blocking. Test both under load.
  • Max requests: Recycle workers after 1,000 requests to prevent memory leaks—a common issue in long-running Django processes.
  • Socket permissions: Ensure the /run/gunicorn/ directory is owned by your app user and the www-data group so Nginx can read the socket.

If you’re new to Linux service management, review systemd services and timers before proceeding. Proper unit files are non-negotiable for production reliability.

What Nginx settings are required when you deploy Django to production?

Nginx serves three critical roles: terminating TLS, serving static/media files directly (bypassing Python entirely), and buffering slow clients so Gunicorn workers aren’t held hostage. Misconfigurations here cause 502 errors, slow page loads, and security gaps.

Optimized Nginx server block

<!-- /etc/nginx/sites-available/myproject -->
upstream django_app {
    server unix:/run/gunicorn/myproject.sock fail_timeout=0;
}

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

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

    # Security headers
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

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

    location /media/ {
        alias /var/www/myproject/media/;
        expires 30d;
    }

    location / {
        proxy_pass http://django_app;
        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;
        proxy_redirect off;
        
        # Buffering protects Gunicorn from slow clients
        proxy_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 8 8k;
    }
}

Critical details often missed:

  1. fail_timeout=0 in upstream prevents Nginx from marking the socket as failed during deployments or brief restarts.
  2. X-Forwarded-Proto ensures Django generates HTTPS URLs in redirects and emails. Without it, you’ll get mixed-content warnings and broken password reset links.
  3. Static file collection: Run python manage.py collectstatic --noinput during deployment, not at runtime. Automate this in your CI pipeline or Ansible playbook.

For TLS automation, see setting up free SSL with Let’s Encrypt. Manual certificate management doesn’t scale and causes outages.

ClientNginxGunicornDjangoHTTPSUnix SocketWSGIStatic Files/static/*DatabaseORM QueriesResponse buffered & compressed
Request lifecycle when you deploy Django to production: static files bypass Python, dynamic requests flow through Gunicorn

How do you manage Django with systemd for zero-downtime deploys?

Systemd provides automatic restarts, logging integration, resource limits, and dependency ordering. A well-written unit file is what separates hobbyist setups from production-grade deployments.

Robust systemd service unit

<!-- /etc/systemd/system/myproject.service -->
[Unit]
Description=Django Application (Gunicorn)
After=network.target postgresql.service redis.service
Requires=postgresql.service

[Service]
User=myproject
Group=www-data
RuntimeDirectory=/run/gunicorn
WorkingDirectory=/var/www/myproject
EnvironmentFile=/etc/myproject/env
ExecStart=/var/www/myproject/venv/bin/gunicorn -c /etc/gunicorn/myproject.py myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
KillMode=mixed
TimeoutStopSec=30

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/www/myproject/media /var/log/gunicorn /run/gunicorn

[Install]
WantedBy=multi-user.target

Key operational practices:

  • EnvironmentFile: Store secrets (DATABASE_URL, SECRET_KEY) here with chmod 600 ownership by root. Never put secrets in the unit file or git.
  • ExecReload: Send HUP for graceful reloads during deploys. New code loads without dropping active connections.
  • ReadWritePaths: Restrict write access to only necessary directories. This contains damage if your app is compromised.
  • Socket activation (optional): For high-traffic sites, pair with a .socket unit so systemd creates the socket before starting Gunicorn, eliminating race conditions on boot.

Always validate with systemd-analyze verify myproject.service before enabling. Syntax errors silently fail in production.

What security hardening steps are mandatory when you deploy Django to production?

Django’s defaults are safe for development but insufficient for production. In 2026, compliance frameworks (SOC 2, ISO 27001) and automated scanners expect these controls. Skipping them invites breaches and failed audits.

ControlDjango Setting / ActionWhy It Matters
Debug modeDEBUG = FalsePrevents stack trace exposure and sensitive data leakage
Allowed hostsALLOWED_HOSTS = ['example.com']Blocks HTTP Host header attacks and cache poisoning
Secure cookiesSESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Ensures cookies transmit only over HTTPS
HSTSSECURE_HSTS_SECONDS = 63072000Forces browsers to use HTTPS, preventing downgrade attacks
Content security policydjango-csp middlewareMitigates XSS by restricting script sources
Secret key rotationUse environment variables + vaultLimits blast radius if secrets leak; enables rotation without downtime

Beyond Django settings, harden the OS layer:

  • Run Gunicorn as a non-root user with minimal privileges.
  • Enable UFW or nftables to allow only ports 80/443. See configuring a firewall with UFW.
  • Automate patching with unattended-upgrades for security updates.
  • Scan dependencies regularly with safety check or Snyk in CI.

For teams handling Nepali user data or fintech workloads, also review data protection basics for Nepal fintech to align with local regulatory expectations around encryption and access logging.

Development (Insecure)python manage.py runserver 0.0.0.0:8000DEBUG = TrueNo TLS / No Security HeadersSecrets in settings.pySingle-threaded, No Crash RecoveryProduction (Hardened)Gunicorn + Nginx + SystemdDEBUG = False + ALLOWED_HOSTSTLS + HSTS + CSP HeadersSecrets in Env File / VaultMulti-worker + Auto-restart + LoggingDeploy
Side-by-side comparison of development versus production configurations when you deploy Django to production

Deploy Django to Production With Confidence

The gap between a working demo and a production-ready Django application isn’t complexity—it’s discipline. Every configuration above has been battle-tested across financial services, e-commerce, and SaaS platforms serving users globally and within Nepal. Start with the Gunicorn-Nginx-systemd trio, enforce security headers from day one, and treat your deployment artifacts as code. If your team needs help auditing an existing setup or building a compliant pipeline from scratch, reach out to discuss your infrastructure. Reliable deployments aren’t accidental; they’re engineered.

Frequently Asked Questions

Use Gunicorn behind Nginx on Ubuntu 24.04 LTS with PostgreSQL 17 and Redis 8 for caching. Containerize with Docker and orchestrate via Kubernetes or Docker Compose depending on scale. This combination offers stability, performance, and broad community support for Django 5.2 deployments.

Bind Gunicorn to a Unix socket at /run/gunicorn.sock using systemd. Set workers to (2 x CPU cores) + 1 and use the sync worker class unless async views are enabled. Configure timeout to 120 seconds and enable access logging for observability.

No, but it ensures environment consistency across dev and prod. Direct installs work fine for single-server setups using virtual environments and system packages. Docker adds complexity but simplifies scaling, CI/CD pipelines, and dependency isolation for teams managing multiple services.

PostgreSQL 17 is the standard choice due to full JSONB support, advanced indexing, and Django ORM compatibility. Avoid SQLite except for development. MySQL works but lacks some Django features like native array fields and partial indexes available in Postgres.

Store secrets in environment variables injected via systemd unit files or cloud secret managers like AWS Secrets Manager. Never commit .env files to version control. Use django-environ to parse values and validate required settings at startup to prevent silent failures.

Let’s Encrypt via Certbot provides free automated TLS certificates with auto-renewal. Configure Nginx as the TLS terminator and proxy HTTP traffic to Gunicorn internally. For internal services, consider Caddy which handles certificate provisioning automatically without manual cron jobs or hooks.

A basic VPS with 2GB RAM and 1 vCPU costs $10–$20 monthly on providers like Hetzner or DigitalOcean. Managed platforms like Railway or Render start at $25+ but reduce ops overhead. Database add-ons and bandwidth overages significantly increase total cost beyond base compute pricing.

Check if Gunicorn is running and listening on the correct socket path defined in Nginx config. Verify file permissions on the socket allow www-data access. Review journalctl logs for Gunicorn crashes caused by memory limits, missing dependencies, or unhandled exceptions during request processing.

Run collectstatic during deployment to copy assets to a dedicated directory. Configure Nginx to serve /static/ directly with expires headers and gzip compression. Offload media files to S3-compatible storage using django-storages to avoid filling application server disk space with user uploads.

Yes, for any task exceeding 30 seconds or requiring retries. Configure Celery with Redis 8 as broker and result backend. Always run separate worker processes and monitor queue depth with Flower. Never execute long-running logic synchronously inside web requests to avoid blocking Gunicorn workers.

Run migrate before restarting Gunicorn using a pre-deploy hook or CI step. Use atomic transactions and avoid destructive operations in single migrations. Test migrations against a staging database snapshot first. Consider zero-downtime strategies like expand-contract patterns for schema changes on high-traffic applications.

Sentry captures exceptions and performance traces with minimal configuration. Prometheus with django-prometheus exposes metrics for Grafana dashboards. Combine with structured logging via structlog and ship to Loki or Datadog. Health check endpoints should verify database connectivity and cache availability for load balancer probes.

Rarely feasible due to lack of WSGI server control and custom package installation. Most shared hosts restrict process spawning needed for Gunicorn. Choose a VPS or managed Python platform instead. Shared hosting may work only for trivial apps with no background tasks or specific system dependencies.

Enable django-debug-toolbar in staging to identify N+1 queries. Use select_related and prefetch_related strategically based on query analysis. Add database indexes for frequently filtered fields. Monitor slow query logs in PostgreSQL and consider connection pooling with PgBouncer to reduce overhead under concurrent load.

Use systemd Type=notify with Gunicorn graceful reload signal USR2. Deploy new code to a separate directory then symlink and trigger reload. Alternatively use blue-green deployments with Nginx upstream switching. Always maintain at least one healthy worker serving requests during the transition period to prevent errors.