Django Production Setup – A Complete Guide to Going Live
February 28, 20267 minIntermediateBackend Technologies

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.

Arbaz Murme

Written by Arbaz Murme

MERN Stack Developer

Arbaz Murme is a Full-Stack Engineer specializing in high-performance React & Next.js web applications, scalable backend microservices, and modern UI engineering.