← Back to all blogs
Django Production Setup – A Complete Guide to Going Live
Sat Feb 28 20267 minIntermediate

Django Production Setup – A Complete Guide to Going Live

A professional, SEO‑optimized tutorial that walks you through every component required to launch a Django application in a production environment.

#django#production#deployment#gunicorn#nginx#docker#security#ci/cd

Introduction

Overview

Deploying a Django project to production is far more than just running python manage.py runserver. A robust production environment must address security, performance, scalability, and observability. This guide covers the essential steps for building a production‑ready stack, from hardened settings to container orchestration.

What We’ll Build

  • A Dockerized Django application
  • Gunicorn as the WSGI server
  • Nginx as a reverse proxy and static files handler
  • Secure settings using environment variables and django‑environ
  • Systemd service files for process supervision
  • Basic monitoring with Prometheus and Grafana

By the end of the article you’ll have a reproducible architecture diagram and ready‑to‑use code snippets.

Configuring Django for Production

Hardened Settings

Django’s default settings.py is tailored for development. For production you need:

1. Separate Settings Files

python

settings/init.py

from .base import *

Choose the appropriate config at runtime

if os.getenv('DJANGO_ENV') == 'production': from .production import * else: from .development import *

Create three files: base.py, development.py, and production.py. Keep secrets out of version control.

2. Using Environment Variables

python

settings/base.py

import environ env = environ.Env( DEBUG=(bool, False), SECRET_KEY=(str, ''), ALLOWED_HOSTS=(list, []), )

Read .env file only in development

if env.bool('DJANGO_READ_DOT_ENV_FILE', default=False): environ.Env.read_env()

DEBUG = env('DEBUG') SECRET_KEY = env('SECRET_KEY') ALLOWED_HOSTS = env('ALLOWED_HOSTS')

3. Security‑Focused Flags

python

settings/production.py

SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_BROWSER_XSS_FILTER = True X_FRAME_OPTIONS = 'DENY'

4. Database Configuration

python

settings/production.py (continued)

DATABASES = { 'default': env.db('DATABASE_URL') }

The DATABASE_URL variable typically looks like postgres://user:pass@db:5432/mydb.

5. Static & Media Files

python STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_URL = '/static/' MEDIA_ROOT = BASE_DIR / 'media' MEDIA_URL = '/media/'

Collect static assets during the Docker build: bash python manage.py collectstatic --noinput

Web Server and Application Server Integration

Architecture Diagram

+-------------------+ +------------------+ +-------------------+ | Load Balancer | --> | Nginx (SSL) | --> | Gunicorn Workers | +-------------------+ +------------------+ +-------------------+ | | | | | | v v v +----------------+ +----------------+ +----------------+ | Docker Pods | <---- | Static Files | | Django App | +----------------+ +----------------+ +----------------+

The diagram shows a typical three‑tier layout:

  1. Load balancer (e.g., AWS ELB) terminates TLS.
  2. Nginx handles static assets, gzip compression, and forwards dynamic requests to Gunicorn.
  3. Gunicorn runs a configurable number of worker processes.

1. Gunicorn Systemd Service

Create /etc/systemd/system/gunicorn.service on the host or inside the container (if using host‑level systemd): ini [Unit] Description=gunicorn daemon for Django project After=network.target

[Service] User=www-data Group=www-data WorkingDirectory=/app Environment="DJANGO_SETTINGS_MODULE=myproject.settings.production" ExecStart=/usr/local/bin/gunicorn
--workers 4
--bind unix:/run/gunicorn.sock
myproject.wsgi:application

[Install] WantedBy=multi-user.target

Enable and start the service: bash systemctl daemon-reload systemctl enable gunicorn systemctl start gunicorn

2. Nginx Configuration

Place this file in /etc/nginx/sites-available/django.conf and link it to sites-enabled: nginx server { listen 80; server_name example.com www.example.com;

# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;

}

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

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;

# Serve static files directly
location /static/ {
    alias /app/staticfiles/;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

location /media/ {
    alias /app/media/;
}

# Proxy to Gunicorn via Unix socket
location / {
    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_pass http://unix:/run/gunicorn.sock;
}

}

Test and reload Nginx: bash nginx -t && systemctl reload nginx

Performance, Security, and Scaling

Optimizing the Stack

1. Worker Count & Async Workers

Gunicorn’s default synchronous workers are fine for low‑traffic sites. For I/O‑bound workloads consider gevent or uvicorn. bash

Example with gevent workers

gunicorn myproject.wsgi:application
--workers 4
--worker-class gevent
--bind 0.0.0.0:8000

2. Caching Layer

Integrate Redis for session storage and cache backend: python

settings/production.py (excerpt)

CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': env('REDIS_URL'), 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } } } SESSION_ENGINE = 'django.contrib.sessions.backends.cache'

3. Database Connection Pooling

Use pgbouncer or Django‑ORM connection pooling to reduce latency under load.

4. Logging & Monitoring

Create a structured logging format that writes JSON to stdout (Docker friendly): python

settings/production.py (logging)

LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'json': { '()': 'pythonjsonlogger.jsonlogger.JsonFormatter', 'fmt': '%(asctime)s %(levelname)s %(name)s %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'json', }, }, 'root': { 'handlers': ['console'], 'level': 'INFO', }, }

Combine with Prometheus exporter (django-prometheus) and expose metrics at /metrics.

5. CI/CD Pipeline Blueprint

mermaid flowchart LR A[Git Push] --> B[GitHub Actions] B --> C{Run Tests} C -->|Success| D[Build Docker Image] D --> E[Push to Registry] E --> F[Deploy to Kubernetes]

The pipeline runs linting, unit tests, builds a minimal image (python:3.11-slim), and triggers a rolling update.

Scaling Strategies

  1. Horizontal Scaling - Add more replica pods behind the load balancer. Docker‑Compose can be swapped for Kubernetes Deployment with replicas: 3.
  2. Vertical Scaling - Increase CPU/memory limits if you notice high latency.
  3. Feature Flags - Deploy new code behind a flag to minimize risk.

Security Checklist

  • ✅ Store secrets in a vault (AWS Secrets Manager, HashiCorp Vault).
  • ✅ Enforce SECURE_CONTENT_TYPE_NOSNIFF and SECURE_REFERRER_POLICY.
  • ✅ Run containers as a non‑root user.
  • ✅ Regularly apply OS and dependency updates (pip list --outdated).
  • ✅ Use OWASP Dependency‑Check in CI to detect vulnerable packages.

FAQs

Frequently Asked Questions

Q1: Do I need to run collectstatic inside the Docker image or at container startup?

A: It is recommended to run collectstatic during the image build so that static files become immutable artifacts. This eliminates the need for a writeable volume at runtime and speeds up container start‑up.

Q2: How can I safely rotate the SECRET_KEY without breaking user sessions?

A: Rotate the key by first generating a new one and storing it in the environment. Then, enable Django’s django.core.signing fallback by adding the old key to SECRET_KEY_FALLBACKS. After a grace period, remove the old entry. This allows existing signed cookies to remain valid while new ones use the fresh secret.

Q3: My Gunicorn workers keep crashing with MemoryError. What should I do?

A: Examine the memory profile of your application. Common fixes include:

  • Reducing the number of workers to match available RAM (workers = (CPU * 2) + 1).
  • Switching to an async worker class if many requests are I/O bound.
  • Enabling request timeout (--timeout 30).
  • Using django‑silk or py‑spy to pinpoint memory‑leaking code.

Q4: Can I serve WebSockets with this setup?

A: Yes. Nginx can proxy WebSocket upgrades when you add the following directives to the location block: nginx proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";

Pair Nginx with Daphne or Uvicorn behind Gunicorn for asynchronous handling.

Conclusion

Wrapping Up

Building a production‑ready Django environment involves more than copying code to a server. By separating configuration, leveraging environment variables, and orchestrating Gunicorn, Nginx, and Docker, you gain a stack that is secure, performant, and easy to scale.

Key takeaways:

  • Keep secrets out of the repository and use django‑environ for runtime configuration.
  • Harden Django with the recommended security flags and TLS termination at the reverse proxy.
  • Use systemd or container orchestrators to supervise Gunicorn workers and enable graceful reloads.
  • Implement caching, structured logging, and metrics to gain visibility into production behavior.
  • Automate the entire pipeline with CI/CD to ensure repeatable deployments.

Adopt this blueprint, tailor it to your cloud provider, and you’ll be ready to serve millions of users with confidence.