Python WSGI/ASGI

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_HEADER di Django settings agar request.is_secure() dan request.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 reload bukan 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
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact