Deployment

Using WSGI and ASGI Servers

Production Django applications require robust application servers to handle HTTP requests efficiently. This chapter covers configuring and deploying Django applications with WSGI servers (Gunicorn, uWSGI) for traditional synchronous applications and ASGI servers (Uvicorn, Daphne, Hypercorn) for asynchronous applications with WebSocket support.

Using WSGI and ASGI Servers

Production Django applications require robust application servers to handle HTTP requests efficiently. This chapter covers configuring and deploying Django applications with WSGI servers (Gunicorn, uWSGI) for traditional synchronous applications and ASGI servers (Uvicorn, Daphne, Hypercorn) for asynchronous applications with WebSocket support.

Understanding WSGI vs ASGI

WSGI (Web Server Gateway Interface)

WSGI is the traditional Python web server interface for synchronous applications:

# wsgi.py
import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')
application = get_wsgi_application()

Use WSGI when:

  • Building traditional synchronous Django applications
  • No real-time features or WebSocket requirements
  • Using synchronous database operations and third-party libraries
  • Maximum compatibility with existing infrastructure

ASGI (Asynchronous Server Gateway Interface)

ASGI supports both synchronous and asynchronous applications with WebSocket capabilities:

# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import myapp.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(
            myapp.routing.websocket_urlpatterns
        )
    ),
})

Use ASGI when:

  • Building applications with real-time features
  • Using WebSockets for live updates
  • Implementing async views and database operations
  • Requiring high concurrency for I/O-bound operations

Gunicorn (WSGI Server)

Gunicorn is the most popular WSGI server for Django applications, offering excellent performance and reliability.

Basic 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
timeout = 30
keepalive = 2

# Restart workers after this many requests, to prevent memory leaks
max_requests = 1000
max_requests_jitter = 50

# Logging
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
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 = '/var/run/gunicorn/django_app.pid'
user = 'django'
group = 'django'
tmp_upload_dir = None

# SSL (if terminating SSL at application level)
# keyfile = '/path/to/keyfile'
# certfile = '/path/to/certfile'

# Environment variables
raw_env = [
    'DJANGO_SETTINGS_MODULE=myproject.settings.production',
]

Advanced Gunicorn Configuration

# gunicorn_advanced.conf.py
import multiprocessing
import os

# Determine optimal worker count based on workload
def get_workers():
    cpu_count = multiprocessing.cpu_count()
    # For CPU-bound applications
    if os.environ.get('WORKLOAD_TYPE') == 'cpu':
        return cpu_count
    # For I/O-bound applications (default)
    else:
        return cpu_count * 2 + 1

# Server socket configuration
bind = [
    "127.0.0.1:8000",  # Local interface
    "unix:/var/run/gunicorn/django_app.sock"  # Unix socket
]
backlog = 2048

# Worker configuration
workers = get_workers()
worker_class = "gevent"  # Async worker for better I/O handling
worker_connections = 1000
timeout = 120  # Longer timeout for complex requests
keepalive = 5

# Memory management
max_requests = 1000
max_requests_jitter = 100
preload_app = True  # Preload application for memory efficiency

# Graceful restarts
graceful_timeout = 30

# Logging configuration
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"
capture_output = True
enable_stdio_inheritance = True

# Custom log format with timing
access_log_format = (
    '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s '
    '"%(f)s" "%(a)s" %(D)s %(p)s'
)

# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190

# Performance tuning
worker_tmp_dir = "/dev/shm"  # Use RAM for temporary files
forwarded_allow_ips = "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"

# Hooks for custom behavior
def on_starting(server):
    server.log.info("Server is starting")

def on_reload(server):
    server.log.info("Server is reloading")

def worker_int(worker):
    worker.log.info("Worker received INT or QUIT signal")

def pre_fork(server, worker):
    server.log.info("Worker spawned (pid: %s)", worker.pid)

def post_fork(server, worker):
    server.log.info("Worker spawned (pid: %s)", worker.pid)
    # Initialize worker-specific resources here

def worker_abort(worker):
    worker.log.info("Worker received SIGABRT signal")

Systemd Service for Gunicorn

# /etc/systemd/system/django-app.service
[Unit]
Description=Django App Gunicorn daemon
Requires=django-app.socket
After=network.target

[Service]
Type=notify
User=django
Group=django
RuntimeDirectory=gunicorn
WorkingDirectory=/opt/django_app
ExecStart=/opt/django_app/venv/bin/gunicorn \
    --config /opt/django_app/gunicorn.conf.py \
    --pid /run/gunicorn/django_app.pid \
    myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
RestartSec=5

# Environment
Environment=DJANGO_SETTINGS_MODULE=myproject.settings.production
EnvironmentFile=/opt/django_app/.env

# Security
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/django_app /var/log/gunicorn /run/gunicorn

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/django-app.socket
[Unit]
Description=Django App Gunicorn socket

[Socket]
ListenStream=/run/gunicorn/django_app.sock
SocketUser=www-data
SocketMode=600

[Install]
WantedBy=sockets.target

uWSGI (WSGI Server)

uWSGI is a powerful application server with extensive configuration options and built-in features.

uWSGI Configuration

# uwsgi.ini
[uwsgi]
# Application settings
module = myproject.wsgi:application
pythonpath = /opt/django_app
chdir = /opt/django_app
virtualenv = /opt/django_app/venv

# Server settings
http = :8000
socket = /run/uwsgi/django_app.sock
chmod-socket = 666
vacuum = true
die-on-term = true

# Process management
master = true
processes = 4
threads = 2
enable-threads = true
thunder-lock = true

# Memory management
max-requests = 1000
max-requests-delta = 100
reload-on-rss = 512
evil-reload-on-rss = 1024

# Logging
logto = /var/log/uwsgi/django_app.log
log-maxsize = 50000000
log-backupname = /var/log/uwsgi/django_app.log.old
logformat = %(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)" %(msecs)ms

# Security
uid = django
gid = django
umask = 002

# Performance
buffer-size = 32768
post-buffering = 8192
harakiri = 60
harakiri-verbose = true
reload-mercy = 8

# Static files (if not using separate web server)
static-map = /static=/opt/django_app/staticfiles
static-expires-uri = /static/.*\.(css|js|png|jpg|jpeg|gif|ico|woff|ttf) 7776000

# Health checks
stats = 127.0.0.1:9191
stats-http = true

# Environment
env = DJANGO_SETTINGS_MODULE=myproject.settings.production
for-readline = /opt/django_app/.env
  env = %(_)
endfor =

Advanced uWSGI Configuration

# uwsgi_advanced.ini
[uwsgi]
# Application
module = myproject.wsgi:application
pythonpath = /opt/django_app
chdir = /opt/django_app
virtualenv = /opt/django_app/venv

# Networking
http = :8000
socket = /run/uwsgi/django_app.sock
chmod-socket = 664
vacuum = true
die-on-term = true

# Process management
master = true
processes = %(%k * 2)  # CPU count * 2
threads = 4
enable-threads = true
thread-stacksize = 512
thunder-lock = true
single-interpreter = true

# Memory optimization
memory-report = true
max-requests = 1000
max-requests-delta = 100
reload-on-rss = 512
evil-reload-on-rss = 1024
reload-on-exception = true

# Caching
cache2 = name=mycache,items=1000,keysize=64,blocksize=1024
cache2 = name=sessions,items=10000,keysize=128,blocksize=2048

# Logging
logto = /var/log/uwsgi/django_app.log
log-maxsize = 50000000
log-backupname = /var/log/uwsgi/django_app.log.old
log-reopen = true
logformat = %(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)" %(msecs)ms

# Monitoring
stats = /run/uwsgi/stats.sock
stats-http = true
memory-report = true
carbon = 127.0.0.1:2003

# Security
uid = django
gid = django
umask = 002
cap = setgid,setuid

# Performance tuning
buffer-size = 65536
post-buffering = 8192
harakiri = 120
harakiri-verbose = true
reload-mercy = 8
worker-reload-mercy = 8

# Offloading
offload-threads = 4
file-serve-mode = x-accel-redirect

# Spooler for background tasks
spooler = /opt/django_app/spooler
spooler-processes = 2
spooler-frequency = 30

# Cheaper subsystem for dynamic scaling
cheaper-algo = busyness
cheaper = 2
cheaper-initial = 2
cheaper-step = 1
cheaper-overload = 30

Uvicorn (ASGI Server)

Uvicorn is a lightning-fast ASGI server built on uvloop and httptools.

Basic Uvicorn Configuration

# uvicorn_config.py
import multiprocessing

# Server configuration
host = "0.0.0.0"
port = 8000
workers = multiprocessing.cpu_count()

# ASGI application
app = "myproject.asgi:application"

# Logging
log_level = "info"
access_log = True
log_config = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        },
        "access": {
            "format": '%(asctime)s - %(client_addr)s - "%(request_line)s" %(status_code)s',
        },
    },
    "handlers": {
        "default": {
            "formatter": "default",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "uvicorn": {"handlers": ["default"], "level": "INFO"},
        "uvicorn.error": {"level": "INFO"},
        "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
    },
}

# SSL configuration (if needed)
# ssl_keyfile = "/path/to/keyfile.key"
# ssl_certfile = "/path/to/certfile.crt"

# Performance
loop = "uvloop"  # Use uvloop for better performance
http = "httptools"  # Use httptools for HTTP parsing
ws = "websockets"  # WebSocket implementation

# Limits
limit_concurrency = 1000
limit_max_requests = 10000
timeout_keep_alive = 5

Production Uvicorn with Gunicorn

# gunicorn_uvicorn.conf.py
import multiprocessing

# Use Uvicorn workers with Gunicorn for production
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count()
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000

# Timeouts
timeout = 120
keepalive = 5
graceful_timeout = 30

# Logging
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"

# Performance
max_requests = 1000
max_requests_jitter = 50
preload_app = True

# Process management
proc_name = "django_asgi_app"
pidfile = "/var/run/gunicorn/django_asgi.pid"

# Environment
raw_env = [
    "DJANGO_SETTINGS_MODULE=myproject.settings.production",
]

Systemd Service for Uvicorn

# /etc/systemd/system/django-asgi.service
[Unit]
Description=Django ASGI App
After=network.target

[Service]
Type=exec
User=django
Group=django
WorkingDirectory=/opt/django_app
ExecStart=/opt/django_app/venv/bin/gunicorn \
    --config /opt/django_app/gunicorn_uvicorn.conf.py \
    myproject.asgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5

# Environment
Environment=DJANGO_SETTINGS_MODULE=myproject.settings.production
EnvironmentFile=/opt/django_app/.env

# Security
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/django_app /var/log/gunicorn

[Install]
WantedBy=multi-user.target

Daphne (ASGI Server)

Daphne is the reference ASGI server developed by the Django Channels team.

Daphne Configuration

# daphne_config.py
import os

# Server settings
bind = "0.0.0.0"
port = 8000
unix_socket = "/run/daphne/django_app.sock"

# Application
application = "myproject.asgi:application"

# Process settings
verbosity = 1
access_log = "/var/log/daphne/access.log"
ping_interval = 20
ping_timeout = 30

# WebSocket settings
websocket_timeout = 86400  # 24 hours
websocket_connect_timeout = 5

# HTTP settings
http_timeout = 120
root_path = ""

# SSL settings (if needed)
# ssl_keyfile = "/path/to/keyfile.key"
# ssl_certfile = "/path/to/certfile.crt"

# Proxy settings
proxy_headers = True
forwarded_allow_ips = ["127.0.0.1", "10.0.0.0/8"]

Running Daphne with Supervisor

# /etc/supervisor/conf.d/django-daphne.conf
[program:django-daphne]
command=/opt/django_app/venv/bin/daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
directory=/opt/django_app
user=django
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/daphne/django_app.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
environment=DJANGO_SETTINGS_MODULE="myproject.settings.production"

Performance Optimization

Worker Process Optimization

# performance/workers.py
import multiprocessing
import os

def calculate_workers():
    """Calculate optimal number of workers"""
    cpu_count = multiprocessing.cpu_count()
    
    # Get workload type from environment
    workload_type = os.environ.get('WORKLOAD_TYPE', 'mixed')
    
    if workload_type == 'cpu':
        # CPU-bound workload
        return cpu_count
    elif workload_type == 'io':
        # I/O-bound workload
        return cpu_count * 4
    else:
        # Mixed workload (default)
        return cpu_count * 2 + 1

def get_worker_class():
    """Determine optimal worker class"""
    if os.environ.get('ASYNC_SUPPORT') == 'true':
        return 'uvicorn.workers.UvicornWorker'
    elif os.environ.get('GEVENT_SUPPORT') == 'true':
        return 'gevent'
    else:
        return 'sync'

# Dynamic configuration
WORKERS = calculate_workers()
WORKER_CLASS = get_worker_class()

Memory Management

# performance/memory.py
import psutil
import os

def get_memory_limits():
    """Calculate memory limits based on available RAM"""
    total_memory = psutil.virtual_memory().total
    available_memory = total_memory * 0.8  # Use 80% of total memory
    
    workers = int(os.environ.get('WEB_CONCURRENCY', 4))
    memory_per_worker = available_memory / workers
    
    return {
        'max_requests': max(500, int(memory_per_worker / (50 * 1024 * 1024))),  # 50MB per request
        'max_requests_jitter': 50,
        'worker_memory_limit': int(memory_per_worker),
    }

def monitor_memory_usage():
    """Monitor and log memory usage"""
    process = psutil.Process()
    memory_info = process.memory_info()
    
    return {
        'rss': memory_info.rss,
        'vms': memory_info.vms,
        'percent': process.memory_percent(),
    }

Load Testing Configuration

# scripts/load_test.py
import asyncio
import aiohttp
import time
from concurrent.futures import ThreadPoolExecutor

async def test_endpoint(session, url, semaphore):
    """Test single endpoint"""
    async with semaphore:
        start_time = time.time()
        try:
            async with session.get(url) as response:
                await response.text()
                return {
                    'status': response.status,
                    'time': time.time() - start_time,
                    'success': response.status == 200
                }
        except Exception as e:
            return {
                'status': 0,
                'time': time.time() - start_time,
                'success': False,
                'error': str(e)
            }

async def load_test(url, concurrent_requests=100, total_requests=1000):
    """Run load test"""
    semaphore = asyncio.Semaphore(concurrent_requests)
    
    async with aiohttp.ClientSession() as session:
        tasks = [
            test_endpoint(session, url, semaphore)
            for _ in range(total_requests)
        ]
        
        start_time = time.time()
        results = await asyncio.gather(*tasks)
        total_time = time.time() - start_time
        
        # Calculate statistics
        successful_requests = sum(1 for r in results if r['success'])
        failed_requests = total_requests - successful_requests
        avg_response_time = sum(r['time'] for r in results) / len(results)
        requests_per_second = total_requests / total_time
        
        print(f"Load Test Results:")
        print(f"  Total Requests: {total_requests}")
        print(f"  Successful: {successful_requests}")
        print(f"  Failed: {failed_requests}")
        print(f"  Average Response Time: {avg_response_time:.3f}s")
        print(f"  Requests per Second: {requests_per_second:.2f}")
        print(f"  Total Time: {total_time:.2f}s")

if __name__ == "__main__":
    asyncio.run(load_test("http://localhost:8000/"))

Monitoring and Health Checks

Server Health Monitoring

# monitoring/server_health.py
import psutil
import time
from django.http import JsonResponse
from django.views import View

class ServerHealthView(View):
    """Monitor server health metrics"""
    
    def get(self, request):
        # System metrics
        cpu_percent = psutil.cpu_percent(interval=1)
        memory = psutil.virtual_memory()
        disk = psutil.disk_usage('/')
        
        # Network metrics
        network = psutil.net_io_counters()
        
        # Process metrics
        process = psutil.Process()
        process_memory = process.memory_info()
        
        # Load average (Unix only)
        try:
            load_avg = psutil.getloadavg()
        except AttributeError:
            load_avg = [0, 0, 0]
        
        health_data = {
            'timestamp': time.time(),
            'system': {
                'cpu_percent': cpu_percent,
                'memory_percent': memory.percent,
                'memory_available': memory.available,
                'disk_percent': (disk.used / disk.total) * 100,
                'load_average': {
                    '1min': load_avg[0],
                    '5min': load_avg[1],
                    '15min': load_avg[2],
                }
            },
            'process': {
                'memory_rss': process_memory.rss,
                'memory_vms': process_memory.vms,
                'cpu_percent': process.cpu_percent(),
                'num_threads': process.num_threads(),
            },
            'network': {
                'bytes_sent': network.bytes_sent,
                'bytes_recv': network.bytes_recv,
                'packets_sent': network.packets_sent,
                'packets_recv': network.packets_recv,
            }
        }
        
        # Determine health status
        status = 'healthy'
        if cpu_percent > 90:
            status = 'warning'
        if memory.percent > 90:
            status = 'critical'
        
        health_data['status'] = status
        
        return JsonResponse(health_data)

This comprehensive guide covers all major WSGI and ASGI servers for Django deployment, providing production-ready configurations and optimization strategies for different deployment scenarios.