Python WSGI/ASGI #
Python memiliki dua standar interface untuk web server: WSGI (synchronous, untuk Flask/Django lama) dan ASGI (asynchronous, untuk FastAPI/Django async). Caddy berfungsi sebagai reverse proxy di depan application server Python (Gunicorn untuk WSGI, Uvicorn untuk ASGI).
Arsitektur Deployment Python #
Internet → Caddy (TLS, HTTP/2, static files)
│
└─ HTTP/Unix Socket
│
Gunicorn/Uvicorn (application server)
│
Flask / Django / FastAPI (framework)
│
Database / Cache / Services
Flask + Gunicorn (WSGI) #
# Setup virtual environment
python3 -m venv /var/www/myapp/venv
source /var/www/myapp/venv/bin/activate
# Install dependencies
pip install flask gunicorn
# Struktur aplikasi Flask sederhana
# /var/www/myapp/app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/health')
def health():
return jsonify(status='ok')
@app.route('/')
def index():
return '<h1>Hello from Flask!</h1>'
if __name__ == '__main__':
app.run()
# Jalankan dengan Gunicorn (production)
cd /var/www/myapp
gunicorn \
--workers 4 \
--bind unix:/run/myapp/gunicorn.sock \
--access-logfile /var/log/myapp/access.log \
--error-logfile /var/log/myapp/error.log \
--timeout 120 \
app:app
FastAPI + Uvicorn (ASGI) #
pip install fastapi uvicorn[standard] gunicorn
# /var/www/fastapi-app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
@app.post("/items/")
async def create_item(item: Item):
return item
# Gunicorn dengan Uvicorn workers untuk ASGI
gunicorn \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind unix:/run/fastapi-app/uvicorn.sock \
--access-logfile /var/log/fastapi/access.log \
--timeout 120 \
main:app
Django Production Setup #
pip install django gunicorn psycopg2-binary whitenoise
# settings.py — production settings
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
# WhiteNoise untuk static files
MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
# ... other middleware
]
STATIC_ROOT = '/var/www/django-app/staticfiles'
STATIC_URL = '/static/'
# WhiteNoise compression dan caching
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# Security
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Collect static files
cd /var/www/django-app
python manage.py collectstatic --noinput
# Run migrations
python manage.py migrate
# Start Gunicorn
gunicorn \
--workers 4 \
--bind unix:/run/django-app/gunicorn.sock \
--timeout 120 \
myproject.wsgi:application
Caddyfile untuk Python App #
example.com {
log {
output file /var/log/caddy/python-access.log
format json
}
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
-Server
}
# Sajikan static files langsung dari Caddy
# (lebih efisien daripada melewati Gunicorn)
@static path /static/* /media/*
handle @static {
root * /var/www/django-app
file_server {
pass_thru
}
header Cache-Control "public, max-age=31536000, immutable"
}
# Semua request lain ke Gunicorn via Unix socket
handle {
reverse_proxy unix//run/django-app/gunicorn.sock {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Real-IP {remote_host}
header_up Host {host}
health_uri /health/
health_interval 15s
health_timeout 5s
}
}
}
Systemd Service untuk Gunicorn #
# /etc/systemd/system/myapp.service
[Unit]
Description=My Python App (Gunicorn)
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
# Environment variables
EnvironmentFile=/etc/myapp/env
Environment="PATH=/var/www/myapp/venv/bin"
# Pastikan socket directory ada
RuntimeDirectory=myapp
RuntimeDirectoryMode=0755
ExecStart=/var/www/myapp/venv/bin/gunicorn \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind unix:/run/myapp/gunicorn.sock \
--pid /run/myapp/gunicorn.pid \
--access-logfile /var/log/myapp/access.log \
--error-logfile /var/log/myapp/error.log \
--timeout 120 \
--graceful-timeout 30 \
main:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
Menentukan Jumlah Workers Gunicorn #
# Rumus umum untuk workers:
# workers = (2 × CPU_cores) + 1
CPU_CORES=$(nproc)
WORKERS=$((2 * CPU_CORES + 1))
echo "Rekomendasi workers: $WORKERS"
# Untuk ASGI (async) seperti FastAPI:
# Lebih sedikit workers tapi bisa handle lebih banyak concurrent request
# workers = CPU_cores atau (CPU_cores × 2)
# Setiap worker punya event loop sendiri
Deployment Update Python #
#!/bin/bash
# deploy-python.sh
APP_DIR="/var/www/myapp"
SERVICE="myapp"
echo "[1] Pulling latest code..."
cd "$APP_DIR"
git pull origin main
echo "[2] Updating dependencies..."
./venv/bin/pip install -r requirements.txt --quiet
echo "[3] Running migrations..."
./venv/bin/python manage.py migrate --noinput
echo "[4] Collecting static files..."
./venv/bin/python manage.py collectstatic --noinput --quiet
echo "[5] Reloading Gunicorn (zero-downtime)..."
# SIGHUP memicu Gunicorn untuk graceful reload
sudo systemctl reload "$SERVICE"
sleep 3
echo "[6] Health check..."
HTTP_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" https://example.com/health/)
if [ "$HTTP_STATUS" != "200" ]; then
echo "✗ Health check gagal! Status: $HTTP_STATUS"
exit 1
fi
echo "✓ Deployment Python berhasil!"
Ringkasan #
- Gunakan Unix socket (
unix//run/myapp/gunicorn.sock) untuk komunikasi Caddy-Gunicorn — lebih cepat dari TCP untuk server yang sama.- Gunicorn + Uvicorn workers (
-k uvicorn.workers.UvicornWorker) adalah setup terbaik untuk FastAPI dan aplikasi ASGI lainnya.- Set
SECURE_PROXY_SSL_HEADERdi Django settings agarrequest.is_secure()danrequest.build_absolute_uri()mengembalikan URL HTTPS yang benar.- Sajikan static files dari Caddy langsung (bukan melewati Gunicorn) menggunakan
handle @static { file_server }— jauh lebih efisien.- Jumlah workers yang baik adalah (2 × CPU_cores) + 1 untuk WSGI, atau CPU_cores untuk ASGI yang sudah async.
- Gunakan
systemctl reloadbukan restart untuk zero-downtime deployment — Gunicorn gracefully menyelesaikan request yang sedang berjalan sebelum worker baru mengambil alih.
← Sebelumnya: PHP-FPM Berikutnya: WebSocket →
Django REST Framework dengan Caddy #
Untuk Django API backend yang umum digunakan sebagai backend React/Vue:
api.example.com {
log {
output file /var/log/caddy/django-api.log
format json
}
encode gzip zstd
# CORS untuk frontend
@options method OPTIONS
handle @options {
header Access-Control-Allow-Origin "https://app.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization, X-CSRFToken"
header Access-Control-Allow-Credentials "true"
respond "" 204
}
header Access-Control-Allow-Origin "https://app.example.com"
header Access-Control-Allow-Credentials "true"
# Django REST Framework via Gunicorn
reverse_proxy unix//run/django-api/gunicorn.sock {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
health_uri /api/health/
health_interval 15s
}
}
Celery dan Background Tasks #
Untuk aplikasi Python yang menggunakan Celery, status task sering perlu diexpose via API:
# Struktur khas:
# - Gunicorn: serve Django/Flask HTTP requests
# - Celery worker: proses background tasks
# - Celery beat: scheduled tasks
# - Redis/RabbitMQ: message broker
# Semua ini berjalan terpisah dari Caddy
# Caddy hanya reverse proxy ke Gunicorn HTTP
# Pastikan Gunicorn restart tidak memutus Celery worker
sudo systemctl restart myapp-web # Hanya restart web server
sudo systemctl status myapp-celery # Celery tetap jalan