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.
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:
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:
Gunicorn is the most popular WSGI server for Django applications, offering excellent performance and reliability.
# 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',
]
# 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")
# /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 is a powerful application server with extensive configuration options and built-in features.
# 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 =
# 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 is a lightning-fast ASGI server built on uvloop and httptools.
# 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
# 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",
]
# /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 is the reference ASGI server developed by the Django Channels team.
# 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"]
# /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/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()
# 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(),
}
# 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/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.
Preparing for Production
Transitioning a Django application from development to production requires systematic preparation across security, performance, configuration, and infrastructure. This chapter covers essential steps to ensure your application is production-ready, secure, and optimized for real-world usage.
Deploying on Linux Servers
Deploying Django applications on Linux servers provides maximum control, performance, and cost-effectiveness. This chapter covers comprehensive server setup, security hardening, service configuration, and deployment automation for Ubuntu, CentOS, and other Linux distributions.