โ† Semua pembelajaran / Python untuk AI Engineer
Fase 5 ยท Bangun API dengan FastAPI

Run a Server Manually

Menjalankan FastAPI di production: pilihan worker, konfigurasi, dan hal yang sering salah.

Intisari

  • Development: --reload. Production: beberapa worker, tanpa reload.
  • Titik awal jumlah worker: (2 ร— jumlah core) + 1 โ€” lalu ukur dan sesuaikan.
  • Di dalam container, jalankan satu worker per container dan biarkan orkestrator yang menskalakan.
  • Bind ke 0.0.0.0 di container; 127.0.0.1 tidak bisa dijangkau dari luar.
  • Taruh reverse proxy (nginx/Caddy) di depan untuk TLS, dan matikan buffering-nya untuk SSE.

Development

uv run uvicorn myapp.main:app --reload --port 8000

--reload memantau perubahan file dan me-restart otomatis. Jangan dipakai di production โ€” ia memakan CPU dan tidak dirancang untuk itu.

Production

uv run uvicorn myapp.main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4 \
  --log-level info \
  --proxy-headers \
  --forwarded-allow-ips='*'
FlagKenapa
--host 0.0.0.0Wajib di container โ€” 127.0.0.1 tidak bisa dijangkau dari luar
--workers NProses paralel, memanfaatkan banyak core
--proxy-headersPercaya X-Forwarded-* dari reverse proxy
--forwarded-allow-ipsProxy mana yang boleh dipercaya
--timeout-keep-aliveSesuaikan dengan keep-alive proxy di depannya

Berapa worker

(2 ร— jumlah core CPU) + 1

Itu titik awal, bukan jawaban akhir. Untuk aplikasi LLM, pertimbangkan:

Untuk aplikasi LLM yang async penuh, sering kali 2โ€“4 worker sudah cukup โ€” bahkan di mesin berinti banyak. Kalau kamu menaikkan worker dan throughput tidak naik, hambatannya bukan di CPU: kemungkinan besar rate limit penyedia API atau ada kode blocking yang tersembunyi.

Di dalam container

CMD ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]

Satu worker per container. Biarkan orkestrator (Docker Compose, ECS, Kubernetes) yang menjalankan beberapa container. Alasannya: health check jadi akurat per instance, restart lebih bersih, dan penggunaan memori bisa diukur per proses. Menjalankan --workers 4 di dalam container membuat orkestrator hanya melihat satu proses induk โ€” dan tidak tahu kalau satu worker di dalamnya mati.

Gunicorn dengan uvicorn worker

uv add gunicorn

uv run gunicorn myapp.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8000 \
  --timeout 300 \
  --graceful-timeout 30

Gunicorn memberi manajemen proses yang lebih matang: restart worker yang macet, graceful reload, dan penanganan sinyal yang lebih baik. Kalau kamu tidak memakai orkestrator container, ini pilihan yang bagus.

Naikkan --timeout untuk aplikasi LLM. Default gunicorn adalah 30 detik โ€” ia akan membunuh worker yang sedang menunggu respons LLM panjang. Setel ke 300 detik atau lebih.

Reverse proxy

server {
    listen 443 ssl;
    server_name api.contoh.com;

    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;
    }

    location /chat/stream {
        proxy_pass http://127.0.0.1:8000;
        proxy_buffering off;          # โ† WAJIB untuk SSE
        proxy_read_timeout 300s;
        proxy_http_version 1.1;
        proxy_set_header Connection '';
    }
}

Graceful shutdown

Uvicorn menangani SIGTERM dengan berhenti menerima koneksi baru, menyelesaikan request yang sedang berjalan, lalu menjalankan bagian shutdown dari lifespan.

services:
  api:
    stop_grace_period: 30s     # beri waktu request panjang untuk selesai

Untuk aplikasi LLM, beri jeda yang cukup โ€” respons streaming bisa berjalan puluhan detik.

Logging

import logging, json, sys

class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        return json.dumps({
            "waktu": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "pesan": record.getMessage(),
        })

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])

Tulis log ke stdout, bukan ke file. Di dunia container, itu kontraknya โ€” orkestrator yang mengumpulkan, merotasi, dan mengirimkannya ke sistem log terpusat. Aplikasi yang menulis ke file sendiri membuat log-nya hilang saat container diganti.

Daftar periksa production

  1. --host 0.0.0.0, tanpa --reload
  2. Konfigurasi lewat environment variable (pydantic-settings)
  3. Endpoint /health dan /ready
  4. Log terstruktur ke stdout
  5. Reverse proxy untuk TLS; buffering dimatikan untuk rute SSE
  6. Timeout worker yang cukup panjang untuk respons LLM
  7. CORS dibatasi ke origin yang benar โ€” jangan ["*"]
  8. Rate limit per pengguna
  9. Batas ukuran request body
  10. Exception handler global supaya detail internal tidak bocor

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.