Production Deployment
Deploying Django applications to production requires careful configuration for security, performance, and reliability.
Pre-Deployment Checklist
Security Settings
# settings/production.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# Security headers
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
# Session security
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
# Secret key from environment
SECRET_KEY = os.environ.get('SECRET_KEY')
if not SECRET_KEY:
raise ValueError('SECRET_KEY environment variable is required')
Database Configuration
# Production 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', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
'OPTIONS': {
'sslmode': 'require',
},
'CONN_MAX_AGE': 60,
}
}
Static Files and Media
Static Files Configuration
# settings/production.py
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# For AWS S3
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME')
Collecting Static Files
# Before deployment
python manage.py collectstatic --noinput
python manage.py compress # If using django-compressor
Web Server Configuration
Nginx Configuration
# /etc/nginx/sites-available/yourproject
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /path/to/your/certificate.crt;
ssl_certificate_key /path/to/your/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
location /static/ {
alias /path/to/your/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /path/to/your/media/;
expires 7d;
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
Gunicorn Configuration
# gunicorn_config.py
bind = "127.0.0.1:8000"
workers = 3
worker_class = "gevent"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100
timeout = 30
keepalive = 2
preload_app = True
# Start Gunicorn
gunicorn --config gunicorn_config.py myproject.wsgi:application
Docker Deployment
Dockerfile
FROM python:3.9-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& 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 --create-home --shell /bin/bash app
RUN chown -R app:app /app
USER app
EXPOSE 8000
CMD ["gunicorn", "--config", "gunicorn_config.py", "myproject.wsgi:application"]
Docker Compose
# docker-compose.prod.yml
version: '3.8'
services:
db:
image: postgres:13
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:6-alpine
restart: unless-stopped
web:
build: .
environment:
- DEBUG=False
- SECRET_KEY=${SECRET_KEY}
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=db
depends_on:
- db
- redis
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./staticfiles:/var/www/static
- ./ssl:/etc/nginx/ssl
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
Environment Variables
.env File
# .env
DEBUG=False
SECRET_KEY=your-super-secret-key-here
DB_NAME=myproject_db
DB_USER=myproject_user
DB_PASSWORD=secure-database-password
DB_HOST=localhost
DB_PORT=5432
# Email settings
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
EMAIL_USE_TLS=True
# Cache settings
REDIS_URL=redis://localhost:6379/0
# AWS S3 settings
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_STORAGE_BUCKET_NAME=your-bucket-name
AWS_S3_REGION_NAME=us-east-1
Environment Loading
# settings/__init__.py
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Determine settings module
if os.environ.get('DEBUG', 'False').lower() == 'true':
from .development import *
else:
from .production import *
Caching
Redis Configuration
# settings/production.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# Session engine
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
Caching Strategies
# views.py
from django.views.decorators.cache import cache_page
from django.core.cache import cache
@cache_page(60 * 15) # Cache for 15 minutes
def article_list(request):
articles = Article.objects.filter(published=True)
return render(request, 'articles/list.html', {'articles': articles})
def expensive_operation():
cache_key = 'expensive_result'
result = cache.get(cache_key)
if result is None:
# Perform expensive operation
result = calculate_complex_data()
cache.set(cache_key, result, 3600) # Cache for 1 hour
return result
Database Optimization
Database Settings
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'),
'OPTIONS': {
'sslmode': 'require',
},
'CONN_MAX_AGE': 60,
'CONN_HEALTH_CHECKS': True,
}
}
# Database connection pooling
DATABASE_POOL_ARGS = {
'max_overflow': 10,
'pool_pre_ping': True,
'pool_recycle': 300,
}
Database Migrations
# Production migration strategy
python manage.py migrate --check # Check migration status
python manage.py migrate --plan # Show migration plan
python manage.py migrate # Apply migrations
# For zero-downtime deployments
python manage.py migrate --fake-initial # If needed
Monitoring and Logging
Logging Configuration
# settings/production.py
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.handlers.RotatingFileHandler',
'filename': '/var/log/django/django.log',
'maxBytes': 16777216, # 16 MB
'backupCount': 5,
'formatter': 'verbose',
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'include_html': True,
},
},
'loggers': {
'django': {
'handlers': ['file', 'mail_admins'],
'level': 'INFO',
'propagate': True,
},
'myproject': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
Health Checks
# views.py
from django.http import JsonResponse
from django.db import connection
from django.core.cache import cache
def health_check(request):
status = {'status': 'ok', 'checks': {}}
# Database check
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
status['checks']['database'] = 'ok'
except Exception as e:
status['checks']['database'] = str(e)
status['status'] = 'error'
# Cache check
try:
cache.set('health_check', 'ok', 30)
if cache.get('health_check') == 'ok':
status['checks']['cache'] = 'ok'
else:
status['checks']['cache'] = 'failed'
status['status'] = 'error'
except Exception as e:
status['checks']['cache'] = str(e)
status['status'] = 'error'
return JsonResponse(status)
Performance Optimization
Database Query Optimization
# Optimize queries with select_related and prefetch_related
def optimized_article_list(request):
articles = Article.objects.select_related('author').prefetch_related('tags')
return render(request, 'articles/list.html', {'articles': articles})
# Use database indexes
class Article(models.Model):
title = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(unique=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
indexes = [
models.Index(fields=['created_at', 'status']),
models.Index(fields=['author', 'created_at']),
]
Content Delivery Network (CDN)
# settings/production.py
# AWS CloudFront
AWS_S3_CUSTOM_DOMAIN = 'your-cloudfront-domain.cloudfront.net'
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
# Static files CDN
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
Deployment Automation
Deployment Script
#!/bin/bash
# deploy.sh
set -e
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 application server
sudo systemctl restart gunicorn
sudo systemctl restart nginx
# Clear cache
python manage.py shell -c "from django.core.cache import cache; cache.clear()"
echo "Deployment completed successfully!"
CI/CD Pipeline
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.2
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /path/to/your/project
./deploy.sh
Security Hardening
Additional Security Measures
# settings/production.py
# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
# Additional security headers
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin'
# Rate limiting
RATELIMIT_ENABLE = True
RATELIMIT_USE_CACHE = 'default'
Backup Strategy
#!/bin/bash
# backup.sh
# Database backup
pg_dump $DB_NAME | gzip > /backups/db_$(date +%Y%m%d_%H%M%S).sql.gz
# Media files backup
rsync -av /path/to/media/ /backups/media/
# Keep only last 7 days of backups
find /backups -name "*.sql.gz" -mtime +7 -delete
Monitoring and Maintenance
System Monitoring
# Custom management command for monitoring
from django.core.management.base import BaseCommand
from django.core.mail import mail_admins
import psutil
class Command(BaseCommand):
def handle(self, *args, **options):
# Check disk usage
disk_usage = psutil.disk_usage('/')
if disk_usage.percent > 90:
mail_admins(
'High Disk Usage Alert',
f'Disk usage is at {disk_usage.percent}%'
)
# Check memory usage
memory = psutil.virtual_memory()
if memory.percent > 90:
mail_admins(
'High Memory Usage Alert',
f'Memory usage is at {memory.percent}%'
)
This comprehensive guide covers the essential aspects of deploying Django applications to production, ensuring security, performance, and reliability.