Skip to main content

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.