Why Not Just Use the Development Server?
Django's runserver and Flask/FastAPI's development servers are great for development but not suitable for production:
- Single-threaded: Can only handle one request at a time
- Not secure: No protection against attacks
- Not optimized: Slow for serving static files
- Auto-reload: Wastes resources in production
For production, you need a proper WSGI/ASGI server (Gunicorn) and a reverse proxy (Nginx).
The Production Stack
Internet
│
▼
┌─────────────────┐
│ Nginx │ ← Reverse proxy, SSL, static files
│ (Port 80/443) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Gunicorn │ ← WSGI/ASGI server, manages workers
│ (Port 8000) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Your Python │ ← Django, Flask, FastAPI
│ App │
└─────────────────┘
What Each Component Does
- Nginx: Handles incoming requests, SSL/HTTPS, serves static files, load balancing
- Gunicorn: Runs your Python application, manages worker processes
- Your App: The actual application code
Setting Up Gunicorn
# Install Gunicorn
pip install gunicorn
# Basic usage (Django)
gunicorn myproject.wsgi:application
# Basic usage (Flask)
gunicorn app:app
# Basic usage (FastAPI)
pip install uvicorn # ASGI server
gunicorn -k uvicorn.workers.UvicornWorker main:app
# With options
gunicorn myproject.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 4 \
--threads 2 \
--timeout 120 \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log
Gunicorn Configuration File
# gunicorn.conf.py
import multiprocessing
# Server socket
bind = "127.0.0.1:8000"
backlog = 2048
# Workers
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync" # Use "uvicorn.workers.UvicornWorker" for ASGI
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
timeout = 30
graceful_timeout = 30
keepalive = 2
# 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"'
# Process naming
proc_name = "myapp"
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
# Run with: gunicorn -c gunicorn.conf.py myproject.wsgi:application
How Many Workers?
# Rule of thumb
workers = (2 * CPU cores) + 1
# For a 4-core server
workers = (2 * 4) + 1 = 9 workers
# For I/O-bound apps (lots of database/API calls)
# Use more workers or threads:
workers = 4
threads = 4 # 16 concurrent requests
# For CPU-bound apps
# Use fewer workers, more CPU per worker:
workers = CPU cores
threads = 1
Setting Up Nginx
# Install Nginx
sudo apt update
sudo apt install nginx
# Start Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# Check status
sudo systemctl status nginx
Nginx Configuration
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name example.com www.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Logging
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
# Static files
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Media files
location /media/ {
alias /var/www/myapp/media/;
expires 7d;
}
# Proxy to Gunicorn
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;
# WebSocket support (for FastAPI)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Client max body size (file uploads)
client_max_body_size 10M;
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration
sudo systemctl reload nginx
Running Gunicorn with Systemd
Use systemd to run Gunicorn as a service that starts automatically:
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for myapp
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
ExecStart=/var/www/myapp/venv/bin/gunicorn \
--workers 4 \
--bind unix:/run/gunicorn/gunicorn.sock \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log \
myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
sudo systemctl status gunicorn
Using Unix Socket (Recommended)
# Create socket directory
sudo mkdir -p /run/gunicorn
sudo chown www-data:www-data /run/gunicorn
# Nginx config for socket
location / {
proxy_pass http://unix:/run/gunicorn/gunicorn.sock;
# ... rest of proxy settings
}
# Socket is faster than TCP for local connections
Complete Deployment Example
# Project structure
/var/www/myapp/
├── venv/ # Virtual environment
├── myproject/ # Django project
│ ├── settings.py
│ ├── wsgi.py
│ └── ...
├── static/ # Collected static files
├── media/ # User uploads
├── gunicorn.conf.py # Gunicorn config
└── requirements.txt
# 1. Create virtual environment
python3 -m venv /var/www/myapp/venv
source /var/www/myapp/venv/bin/activate
pip install -r requirements.txt
pip install gunicorn
# 2. Collect static files (Django)
python manage.py collectstatic --noinput
# 3. Set proper permissions
sudo chown -R www-data:www-data /var/www/myapp
sudo chmod -R 755 /var/www/myapp
# 4. Create log directories
sudo mkdir -p /var/log/gunicorn /var/log/nginx
sudo chown www-data:www-data /var/log/gunicorn
# 5. Set up systemd service
sudo nano /etc/systemd/system/gunicorn.service
sudo systemctl daemon-reload
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
# 6. Configure Nginx
sudo nano /etc/nginx/sites-available/myapp
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
# 7. Get SSL certificate
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
ASGI Deployment (FastAPI)
# For FastAPI/Starlette (ASGI apps), use Uvicorn workers
pip install uvicorn gunicorn
# Run with Uvicorn workers
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 127.0.0.1:8000
# Or use Uvicorn directly (simpler for single instance)
uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4
# Systemd service for FastAPI
[Service]
ExecStart=/var/www/myapp/venv/bin/gunicorn \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind unix:/run/gunicorn/gunicorn.sock \
main:app
Performance Tuning
# Nginx tuning
worker_processes auto;
worker_connections 1024;
http {
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
# Buffer sizes
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Connection keep-alive
keepalive_timeout 65;
keepalive_requests 100;
# Static file caching
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
}
# Gunicorn tuning
# For I/O bound apps (database, API calls)
workers = 4
threads = 4
worker_class = "gthread"
# For CPU bound apps
workers = cpu_cores
threads = 1
worker_class = "sync"
Troubleshooting
# Check Gunicorn status
sudo systemctl status gunicorn
sudo journalctl -u gunicorn
# Check Nginx status
sudo systemctl status nginx
sudo nginx -t # Test configuration
# Check logs
tail -f /var/log/gunicorn/error.log
tail -f /var/log/nginx/error.log
# Common issues:
# 1. 502 Bad Gateway
# - Gunicorn not running
# - Wrong socket/port in Nginx config
# 2. Permission denied
# - Check file ownership (www-data)
# - Check socket permissions
# 3. Static files not loading
# - Run collectstatic
# - Check Nginx static location path
# 4. Timeout errors
# - Increase Gunicorn timeout
# - Increase Nginx proxy_read_timeout
Master Deployment with Expert Mentorship
Our Full Stack Python program covers production deployment with Gunicorn and Nginx. Learn to deploy scalable, secure Python applications with personalized guidance.
Explore Full Stack Python Program