Deployment & Production
Learn production deployment strategies, server configuration, and monitoring for Django applications. This is a foundational concept in Python web development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world Python/Django experience. Take your time with each section and practice the examples
Production Deployment
Deploying Django applications to production requires careful planning, proper server configuration, and ongoing monitoring to ensure reliability and performance.. This is an essential concept that every Python/Django developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Deployment Strategies & Tools
# Production Deployment Strategies
# 1. Docker Deployment
# Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=myproject.settings.production
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y postgresql-client nginx && rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
# Create non-root user
RUN useradd -m -u 1000 django && chown -R django:django /app
USER django
# Expose port
EXPOSE 8000
# Start command
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/dbname
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
- DEBUG=False
depends_on:
- db
- redis
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
restart: unless-stopped
db:
image: postgres:13
environment:
- POSTGRES_DB=dbname
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:6-alpine
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/staticfiles
- media_volume:/app/media
- ./ssl:/etc/nginx/ssl
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
# 2. Nginx Configuration
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream django {
server web:8000;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-security "max-age=31536000; includeSubDomains";
# Rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://django;
}
location /login/ {
limit_req zone=login burst=5 nodelay;
proxy_pass http://django;
}
# Static files
location /static/ {
alias /app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Media files
location /media/ {
alias /app/media/;
expires 1y;
add_header Cache-Control "public";
}
# Proxy to Django
location / {
proxy_pass http://django;
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;
}
}
}
# 3. Gunicorn Configuration
# gunicorn.conf.py
import multiprocessing
import os
# Server socket
bind = "0.0.0.0:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
preload_app = True
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = "django_app"
# Server mechanics
daemon = False
pidfile = "/tmp/gunicorn.pid"
user = None
group = None
tmp_upload_dir = None
# SSL
keyfile = None
certfile = None
# 4. Production Settings
# settings/production.py
import os
from .base import *
# security
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY')
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST'),
'PORT': os.environ.get('DB_PORT', '5432'),
'OPTIONS': {
'CONN_MAX_AGE': 60,
},
}
}
# Cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.environ.get('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
}
}
# Static files
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# Media files
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# security settings
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/django/app.log',
'formatter': 'verbose',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['file', 'console'],
'level': 'INFO',
},
}
# 5. Deployment Scripts
# deploy.sh
#!/bin/bash
echo "Starting deployment..."
# Pull latest code
git pull origin main
# Install dependencies
pip install -r requirements.txt
# Run migrations
python manage.py migrate
# Collect static files
python manage.py collectstatic --noinput
# Restart services
sudo systemctl restart gunicorn
sudo systemctl restart nginx
echo "Deployment completed!"
# 6. Systemd Service Files
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for Django application
After=network.target
[Service]
User=django
Group=django
WorkingDirectory=/app
ExecStart=/app/venv/bin/gunicorn --config gunicorn.conf.py myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
# 7. Monitoring and Health Checks
# health_check.py
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.core.cache import cache
from django.db import connection
import psutil
import os
@require_http_methods(["GET"])
def health_check(request):
"""comprehensive health check endpoint"""
health_status = {
'status': 'healthy',
'timestamp': timezone.now().isoformat(),
'checks': {}
}
# Database check
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
health_status['checks']['database'] = 'healthy'
except Exception as e:
health_status['checks']['database'] = f'unhealthy: {str(e)}'
health_status['status'] = 'unhealthy'
# Cache check
try:
cache.set('health_check', 'ok', 10)
if cache.get('health_check') == 'ok':
health_status['checks']['cache'] = 'healthy'
else:
health_status['checks']['cache'] = 'unhealthy'
health_status['status'] = 'unhealthy'
except Exception as e:
health_status['checks']['cache'] = f'unhealthy: {str(e)}'
health_status['status'] = 'unhealthy'
# System resources
try:
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
health_status['checks']['system'] = {
'cpu_percent': cpu_percent,
'memory_percent': memory.percent,
'disk_percent': disk.percent
}
# Check if resources are within limits
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_status['status'] = 'degraded'
except Exception as e:
health_status['checks']['system'] = f'unhealthy: {str(e)}'
health_status['status'] = 'unhealthy'
# Return appropriate status code
status_code = 200 if health_status['status'] == 'healthy' else 503
return JsonResponse(health_status, status=status_code)