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:
- Load balancer (e.g., AWS ELB) terminates TLS.
- Nginx handles static assets, gzip compression, and forwards dynamic requests to Gunicorn.
- 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
- Horizontal Scaling - Add more replica pods behind the load balancer. Docker‑Compose can be swapped for Kubernetes Deployment with
replicas: 3. - Vertical Scaling - Increase CPU/memory limits if you notice high latency.
- 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_NOSNIFFandSECURE_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‑silkorpy‑spyto 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‑environfor 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.
