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_ontek başına yeterli değildir. Container ayaktadır ama PostgreSQL hazır olmayabilir; migration patlar. asyncpgyerinepsycopgdriver yazmak:DATABASE_URL'depostgresql+asyncpg://yazmak zorundasınız, yoksa SQLAlchemy async driver bulamayınca anlamsız bir hata verir.- Migration'ı
commandiçinde çalıştırmak: Tek replica için bu kabul. Birden fazla replica açınca her instance aynı andaalembic upgrade headçalıştırır ve advisory lock kullanmazsanız migration tablosunda bozulma riski çıkar. Bu durumda ayrı birmigratejob'ı açın. - Volume'u dış bir path'e bind etmek:
./pg_data:/var/lib/postgresql/datagibi bir bind kullanınca SELinux veya UID/GID sorunları yaşarsınız. Portainer host'unda named volume kalsın. POSTGRES_PASSWORDPortainer 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.
