FastAPI ve PostgreSQL stack'ini Portainer ile yayına almak

Selamlar, bu yazımda FastAPI tarafında bir API'yi PostgreSQL ile beraber Portainer'in stack özelliği üzerinden nasıl ayağa kaldırdığıma bakacağız. Konuya çok yabancı olmayan ama 'compose dosyasını Portainer'a yapıştırınca neden patlıyor?' diyen arkadaşlar için yazdım. Lafı uzatmadan başlayalım.

Açıkçası ben de ilk denemelerimde container'ları ayağa kaldıran depends_on kuralının yeterli olduğunu zannetmiştim. Sonra görüyorsunuz ki API, PostgreSQL daha hazır olmadan migration çalıştırmaya başlıyor ve connection refused ile düşüyor. Buradaki kritik nokta service_healthy koşulu ve pg_isready ile doğru bir healthcheck yazmak.

Stack dosyası

Portainer'da Stacks > Add stack diyip aşağıdaki compose dosyasını yapıştırmanız yeterli. Ben üretim için aşağıdaki gibi bir şablon kullanıyorum:

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: siparis
      POSTGRES_USER: siparis_app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U siparis_app -d siparis']
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build: ./app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - '8000:8000'
    environment:
      DATABASE_URL: postgresql+asyncpg://siparis_app:${DB_PASSWORD}@db:5432/siparis
      APP_SECRET: ${APP_SECRET}
    command: >
      bash -lc 'alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4'

volumes:
  pg_data:

Burada birkaç noktanın altını çizmek isterim. depends_on.condition: service_healthy olmadan API container'ı, veritabanı daha hazır değilken ayağa kalkar. Healthcheck'in pg_isready -U siparis_app -d siparis şeklinde hem kullanıcı hem de veritabanı parametresiyle yazılması sık gözden kaçan bir detay. Sadece pg_isready derseniz socket'i kontrol eder, asıl DB hazır olmadan da yeşil yanabilir.

${DB_PASSWORD} ve ${APP_SECRET} değerlerini Portainer'in 'Environment variables' kısmından girin. Stack içine düz metin şifre yapıştırmak istemezsiniz; hem versiyon kontrolüne kaçar hem de Portainer ekranı üzerinden herkes okur.

Uygulama tarafı

API tarafında elimden geldiğince ince bir şablon tutuyorum. SQLAlchemy 2.x'in async desteği artık gerçekten oturdu; asyncpg ile beraber kullanınca performans gayet tatmin edici.

# app/main.py
from fastapi import FastAPI
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
import os

app = FastAPI(title='Siparis API')
engine = create_async_engine(os.environ['DATABASE_URL'], pool_pre_ping=True)

@app.get('/health')
async def health():
    try:
        async with engine.connect() as conn:
            await conn.execute(text('SELECT 1'))
        return {'status': 'ok'}
    except Exception as exc:
        return {'status': 'down', 'detail': str(exc)}

pool_pre_ping=True parametresi tecrübeyle sabittir; gece yarısı PostgreSQL'in bakım için kısa bir kapanmasından sonra havuzdaki ölü bağlantıların temizlenmesini sağlıyor. Bu küçük parametre olmadan sabaha karşı birçok 'OperationalError: connection invalid' alırsınız.

Sık karşılaşılan hatalar

  • Healthcheck'i unutmak: depends_on tek başına yeterli değildir. Container ayaktadır ama PostgreSQL hazır olmayabilir; migration patlar.
  • asyncpg yerine psycopg driver yazmak: DATABASE_URL'de postgresql+asyncpg:// yazmak zorundasınız, yoksa SQLAlchemy async driver bulamayınca anlamsız bir hata verir.
  • Migration'ı command içinde çalıştırmak: Tek replica için bu kabul. Birden fazla replica açınca her instance aynı anda alembic upgrade head çalıştırır ve advisory lock kullanmazsanız migration tablosunda bozulma riski çıkar. Bu durumda ayrı bir migrate job'ı açın.
  • Volume'u dış bir path'e bind etmek: ./pg_data:/var/lib/postgresql/data gibi bir bind kullanınca SELinux veya UID/GID sorunları yaşarsınız. Portainer host'unda named volume kalsın.
  • POSTGRES_PASSWORD Portainer environment alanında ama compose'da değişken referansı olmamak: Boş string ile container ayağa kalkar, sonra trust authentication zannedip yanılırsınız.

Doğrulama

Stack ayağa kalkınca şu komutlarla durumu hızlıca gözden geçirebilirsiniz:

curl -s http://localhost:8000/health
docker exec -it $(docker ps -qf name=db) pg_isready -U siparis_app -d siparis
docker logs --tail 50 $(docker ps -qf name=api)

/health endpoint'i {'status': 'ok'} dönmüyorsa önce API loglarına bakın; çoğunlukla DATABASE_URL içindeki host adı yanlış ya da şifre escape edilmemiş oluyor. Bende bir keresinde @ karakteri içeren bir şifre yüzünden yarım saat kaybetmiştim, URL-encode etmek lazım.

Kapanış

Bu yazıda FastAPI ve PostgreSQL stack'ini Portainer üzerinden ayağa kaldırırken healthcheck, async driver ve migration konularında dikkat edilmesi gereken noktalara baktık. Bence Portainer GUI'si küçük ve orta ölçekli takımlar için kubectl ihtiyacı doğmadan yeterince güçlü; asıl mesele compose dosyasını olabildiğince açık yazmak. Umarım faydalı olur, görüşmek üzere.