Ana içeriğe geç
Kesit Python
İleri · Üretime hazırlık

Docker ve dağıtım: konteyner, compose, bulut

Python uygulamanı Dockerfile ile konteynere paketler, katman önbelleği ve çok aşamalı yapıyla imajı küçültür, compose ile PostgreSQL'le birlikte çalıştırır, ortam değişkeni, sağlık kontrolü ve stdout loglamayla 12-factor uyumlu biçimde buluta gönderirsin.

Uygulamada aç, 31 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli? "Benim bilgisayarımda çalışıyordu"

Anadolu Market'in sipariş API'sini yazdın; testler yeşil, kendi bilgisayarında her şey kusursuz. Sunucuya kurduğunda ise bambaşka bir Python sürümü, eksik bir sistem kütüphanesi ve farklı bir saat dilimiyle karşılaşıyorsun. Bu derste uygulamanı konteyner (container) içine paketleyip her ortamda aynı şekilde çalıştırmayı, veritabanıyla birlikte ayağa kaldırmayı ve buluta göndermeyi öğreneceksin. Paketleme dersinde wheel ve pyproject.toml ile kütüphane dağıtmayı gördün; burada konu uygulamayı dağıtmak.

Tarayıcıda Docker çalışmaz; bu yüzden komutları gri kutularda göreceksin, alıştırmalarda ise Docker'ın mantığını Python ile kuracaksın: Dockerfile denetleyen bir "linter", compose dosyasından başlatma sırası çıkaran bir planlayıcı, ortam değişkeninden ayar okuyan bir sınıf ve sağlık kontrolü işleyicisi. Bunlar gerçek ekiplerin CI hattında koşturduğu araçların küçük kardeşleri.

Konteyner mi, sanal makine mi?

Sanal makine (virtual machine, VM) bütün bir işletim sistemini çekirdeğiyle birlikte taklit eder. Konteyner ise ana makinenin çekirdeğini paylaşır; yalnızca uygulamanın dosya sistemini, kütüphanelerini ve süreçlerini yalıtır. İmaj (image) salt okunur bir şablondur, konteyner o imajın çalışan bir kopyasıdır: sınıf ile nesne gibi düşünebilirsin.

ÖzellikSanal makineKonteyner
YalıtımAyrı çekirdek, güçlü yalıtımOrtak çekirdek, süreç düzeyinde yalıtım
Açılış süresiDakikalarSaniyeler
BoyutGB düzeyindeOnlarca–yüzlerce MB
Tipik kullanımFarklı işletim sistemleri, eski sistemlerMikroservis, API, toplu iş, CI
Tanım dosyasıKurulum betikleri, Packer şablonuDockerfile

İlk Dockerfile

Dockerfile, imajı adım adım nasıl inşa edeceğini anlatan bir tariftir. Her satır bir komut (instruction) ve bir argümandan oluşur:

# Dockerfile — Anadolu Market API
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home app
USER app
EXPOSE 8000
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000", "market.main:app"]
docker build -t anadolu-market-api:1.4.0 .
docker run --rm -p 8000:8000 --env-file .env anadolu-market-api:1.4.0
docker ps            # çalışan konteynerler
docker logs -f <id>  # stdout'a yazılan loglar
KomutNe yaparDikkat
FROMTemel imajı seçerHer zaman ilk komut; latest yerine sabit sürüm (3.12-slim)
WORKDIRÇalışma klasörünü ayarlar (yoksa oluşturur)RUN cd kalıcı değildir; WORKDIR kullan
COPYDosyaları imaja kopyalar.dockerignore ile gereksizleri dışla
RUNİnşa sırasında komut çalıştırır, yeni katman üretir--no-cache-dir pip önbelleğini imaja koymaz
ENVOrtam değişkeni tanımlarSır (şifre, token) asla buraya yazılmaz
USERSonraki komutların kullanıcısıVarsayılan root'tur; yetkisiz kullanıcıya geç
EXPOSEUygulamanın dinlediği portu belgelerPortu açmaz; -p açar
CMDKonteyner başlarken çalışacak komutJSON (exec) biçimi sinyalleri doğru iletir

Dockerfile düz bir metin olduğu için Python ile kolayca ayrıştırılır. Aşağıdaki kod yorumları atlayıp her satırı (komut, argüman) çiftine ayırıyor; alıştırmalarda bunu bir denetleyiciye dönüştüreceksin.

dockerfile = """
# Anadolu Market
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
"""

adimlar = []
for satir in dockerfile.splitlines():
    satir = satir.strip()
    if not satir or satir.startswith("#"):
        continue
    komut, _, arguman = satir.partition(" ")
    adimlar.append((komut.upper(), arguman))

for no, (komut, arguman) in enumerate(adimlar, 1):
    print(f"{no}. {komut:<8} {arguman}")

Katman önbelleği ve .dockerignore

Her RUN, COPY ve ADD yeni bir katman (layer) üretir. Docker bir adımı yeniden inşa ederken önceki inşadan kalan katmanı kullanabilir: komut metni aynıysa ve kopyalanan dosyalar değişmediyse adım önbellekten gelir. Bir adım değiştiği anda ondan sonraki bütün adımlar yeniden çalışır. Bu yüzden sık değişen şeyler (uygulama kodu) sona, seyrek değişenler (bağımlılıklar) başa yazılır.

adimlar = ["FROM python:3.12-slim", "COPY requirements.txt .",
           "RUN pip install -r requirements.txt", "COPY . .", "CMD python app.py"]

def yeniden_insa(adimlar, ilk_degisen):
    for i, adim in enumerate(adimlar):
        durum = "önbellek" if i < ilk_degisen else "YENİDEN"
        print(f"  {durum:<9} {adim}")

print("Yalnızca kod değişti:")
yeniden_insa(adimlar, 3)
print("requirements.txt değişti:")
yeniden_insa(adimlar, 1)

İlk durumda en pahalı adım olan pip install önbellekten gelir ve inşa saniyeler sürer. COPY . .'yu en başa koysaydın her kod değişikliğinde bütün paketler yeniden inerdi.

.dockerignore, .gitignore gibi çalışır: inşa bağlamına (build context) girmeyecek dosyaları listeler. Hem imajı küçültür hem de .env gibi sır dosyalarının yanlışlıkla imaja girmesini engeller.

.git
.venv
__pycache__/
*.pyc
.env
tests/
*.log

Çok aşamalı yapı (multi-stage build)

Bazı paketler derlenirken derleyici ister (gcc, başlık dosyaları), ama çalışırken istemez. Çok aşamalı yapıda ilk aşama "inşaat alanı"dır; ikinci aşama yalnızca bitmiş ürünü alır. Sonuç: daha küçük ve saldırı yüzeyi daha dar bir imaj.

FROM python:3.12-slim AS insa
RUN apt-get update && apt-get install -y --no-install-recommends build-essential
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=insa /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
RUN useradd --create-home app
USER app
CMD ["python", "-m", "market"]
root ve latest tuzaklarıUSER yazmazsan konteyner içindeki süreç root olarak çalışır; uygulamadaki bir açık, saldırgana konteyner içinde tam yetki verir. FROM python ya da FROM python:latest ise bugün 3.12, yarın 3.13 demektir: aynı Dockerfile bir gün habersizce farklı imaj üretir. Sürümü her zaman sabitle.

docker compose: uygulama + PostgreSQL

Gerçek uygulama tek başına yaşamaz; veritabanı, önbellek, kuyruk ister. docker compose birden fazla servisi tek bir YAML dosyasıyla tanımlar ve birlikte başlatır.

# compose.yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: market
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
    volumes: ["pgdata:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5
  api:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgresql://postgres:$POSTGRES_PASSWORD@db:5432/market
    depends_on:
      db:
        condition: service_healthy
volumes:
  pgdata: {}
docker compose up -d --build   # inşa et ve arka planda başlat
docker compose logs -f api      # api servisinin logları
docker compose down             # durdur (volume'lar kalır)

Servisler birbirine adıyla ulaşır: API, veritabanına localhost ile değil db adıyla bağlanır. depends_on başlatma sırasını belirler; condition: service_healthy ise "veritabanı sağlık kontrolünü geçene kadar bekle" der. Compose dosyası YAML olduğu için yaml.safe_load ile okunur ve bağımlılıklardan başlatma sırası topolojik sıralama (topological sort) ile çıkarılır:

import yaml

metin = """
services:
  api:
    depends_on: [db, cache]
  worker:
    depends_on: [db]
  db: {}
  cache: {}
"""
servisler = yaml.safe_load(metin)["services"]
bagimlilik = {ad: set(s.get("depends_on", [])) for ad, s in servisler.items()}

sira = []
while bagimlilik:
    hazir = sorted(ad for ad, b in bagimlilik.items() if b <= set(sira))
    sira.extend(hazir)
    for ad in hazir:
        del bagimlilik[ad]
print("Başlatma sırası:", sira)
safe_load, load değilyaml.load uygun yükleyiciyle keyfi Python nesnesi oluşturabilir; dışarıdan gelen YAML'ı her zaman yaml.safe_load ile oku.

Ortam değişkenleri, sırlar ve 12-factor

12-factor uygulama (twelve-factor app), bulutta çalışan uygulamalar için yaygın kabul görmüş bir ilkeler listesidir. En sık başvurulanları:

İlkeAnlamıPython'da karşılığı
Yapılandırma ortamdaOrtama göre değişen her şey ortam değişkenidiros.environ, pydantic-settings
Bağımlılıklar açıkHer paket sürümüyle beyan edilirrequirements.txt, kilit dosyası
Destek servisleri eklentiVeritabanı bir URL'dir, kod değişmeden değişirDATABASE_URL
Durumsuz süreçlerOturum, dosya yerel diskte tutulmazRedis, nesne deposu
Port bağlamaUygulama kendi portunu dinlergunicorn/uvicorn -b 0.0.0.0:$PORT
AtılabilirlikHızlı açılır, SIGTERM'de düzgün kapanırsignal işleyici, bağlantıları kapat
Loglar akıştırDosyaya değil stdout'a yazılırlogging.StreamHandler(sys.stdout)

Ortam değişkenleri her zaman metin gelir. "8000" sayıya, "false" mantıksal değere senin çevirmen gerekir; bool("false") True verir çünkü boş olmayan her metin doğrudur. Ayarları tek bir yerde, açılışta doğrulayan bir sınıf yazmak bu hataları erkenden yakalar:

from dataclasses import dataclass

DOGRU = {"1", "true", "yes", "evet", "on"}

@dataclass(frozen=True)
class Ayarlar:
    database_url: str
    port: int = 8000
    debug: bool = False

    @classmethod
    def ortamdan(cls, env):
        if "DATABASE_URL" not in env:
            raise ValueError("DATABASE_URL tanımlı değil")
        return cls(
            database_url=env["DATABASE_URL"],
            port=int(env.get("PORT", "8000")),
            debug=env.get("DEBUG", "").strip().lower() in DOGRU,
        )

ayar = Ayarlar.ortamdan({"DATABASE_URL": "postgresql://db/market", "DEBUG": "false", "PORT": "9000"})
print(ayar)
print("bool('false') =", bool("false"))

Gerçek kodda env yerine os.environ verirsin; testte ise küçük bir sözlük. Bu, bağımlılık enjeksiyonunun (dependency injection) en basit hâlidir. Sırlar (secrets: veritabanı şifresi, API anahtarı) imaja, Dockerfile'a ya da git'e girmez; çalışma anında ortam değişkeni ya da bulutun sır yöneticisiyle (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) verilir.

Sağlık kontrolü ve loglama

Orkestratör (Docker, Kubernetes, Render) uygulamanın yaşayıp yaşamadığını HTTP uç noktalarıyla sorar. İki ayrı soru vardır: canlılık (liveness, /health): "Süreç takıldı mı?" ve hazırlık (readiness, /ready): "Veritabanına ulaşıp istek alabilir misin?" Hazırlık başarısızsa trafik o kopyaya yönlendirilmez; canlılık başarısızsa konteyner yeniden başlatılır.

def hazirlik(kontroller):
    sonuc = {}
    for ad, kontrol in kontroller.items():
        try:
            kontrol()
            sonuc[ad] = "ok"
        except Exception as e:
            sonuc[ad] = f"hata: {e}"
    kod = 200 if all(v == "ok" for v in sonuc.values()) else 503
    return kod, sonuc

def veritabani():
    return True

def onbellek():
    raise ConnectionError("redis:6379 yanıt vermiyor")

print(hazirlik({"db": veritabani}))
print(hazirlik({"db": veritabani, "cache": onbellek}))

Loglar konteyner içinde dosyaya yazılırsa konteyner silinince kaybolur. 12-factor kuralı: stdout'a yaz, toplamayı platform yapsın (Docker docker logs, bulutta CloudWatch, Cloud Logging). Makinenin okuyabilmesi için tek satırlık JSON en iyisidir:

import io, json, logging

class JsonBicim(logging.Formatter):
    def format(self, kayit):
        return json.dumps({"seviye": kayit.levelname, "kaydedici": kayit.name,
                           "mesaj": kayit.getMessage()}, ensure_ascii=False)

akis = io.StringIO()            # gerçekte sys.stdout
isleyici = logging.StreamHandler(akis)
isleyici.setFormatter(JsonBicim())
log = logging.getLogger("market.api")
log.addHandler(isleyici)
log.setLevel(logging.INFO)

log.info("sipariş alındı: %s", "S-1001")
log.warning("stok azaldı: %d adet", 3)
print(akis.getvalue(), end="")

gunicorn, uvicorn ve bulut seçenekleri

Geliştirme sunucusu (flask run, uvicorn --reload) üretim için değildir. Üretimde bir süreç yöneticisi birden fazla işçi (worker) başlatır: WSGI (Flask, Django) için gunicorn, ASGI (FastAPI) için uvicorn ya da uvicorn işçili gunicorn. İşçi sayısı için başlangıç kuralı 2 × çekirdek + 1'dir, ama ölçerek ayarlanır.

gunicorn -w 4 -b 0.0.0.0:8000 market.wsgi:app                    # Flask/Django
uvicorn market.main:app --host 0.0.0.0 --port 8000 --workers 4    # FastAPI
gunicorn -k uvicorn.workers.UvicornWorker -w 4 market.main:app    # ikisi birlikte
PlatformModelNe zaman?
Render, RailwayGit'e bağla, Dockerfile'ı kendisi inşa etsin; yönetilen PostgreSQLPrototip, küçük ekip, hızlı başlangıç
Fly.ioKonteyneri dünyanın farklı bölgelerinde çalıştırırKullanıcıya yakın, düşük gecikme
AWS (ECS/Fargate, App Runner)Konteyner servisleri, RDS veritabanıKurumsal ölçek, ayrıntılı ağ ve yetki
GCP Cloud Runİstek geldikçe ölçeklenen konteynerDeğişken trafik, sıfıra ölçeklenme
Azure Container Apps / App ServiceYönetilen konteyner ve web uygulamasıMicrosoft ekosistemindeki şirketler
Gerçek işteBir bankacılık ekibinde canlıya çıkış genellikle şöyle akar: geliştirici birleştirme isteği (pull request) açar, CI testleri koşturur, Dockerfile'ı hadolint gibi bir denetleyiciden geçirir, imajı git commit kimliğiyle etiketleyip (api:3f2a9c1) şirketin imaj deposuna iter, imaj güvenlik açığı taramasından geçer ve önce test ortamına, onaydan sonra üretime aynı imaj gönderilir. "Aynı imaj" kısmı kritiktir: test edilen şey ile canlıya çıkan şey bit bit aynıdır, yalnızca ortam değişkenleri farklıdır.
Derinleş

İmajın hazır ve compose ile yerelde çalışıyor. Şimdi üç gerçek soru var: uygulamanın önüne ne koyarsın (Nginx), en hızlı nasıl yayına alırsın (PaaS) ve ekip büyüdüğünde konteynerleri kim yönetir (Kubernetes)?

Nginx: ters vekil sunucu (reverse proxy)

gunicorn ya da uvicorn Python kodunu çalıştırmakta iyidir ama internete doğrudan açılmak için tasarlanmamıştır. Önlerine Nginx konur ve şu işleri üstlenir: HTTPS sertifikasını sonlandırmak (TLS termination), statik dosyaları Python'a hiç uğratmadan sunmak, yavaş istemcileri tamponlayıp (buffering) uygulama işçilerini boşta bekletmemek, gzip sıkıştırma, istek boyutu ve hız sınırı.

# /etc/nginx/conf.d/market.conf
upstream api {
    server 127.0.0.1:8000;          # gunicorn/uvicorn; compose'da: server api:8000;
}
server {
    listen 443 ssl;
    server_name market.example.com;
    ssl_certificate     /etc/letsencrypt/live/market.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/market.example.com/privkey.pem;
    client_max_body_size 10m;

    location /static/ {
        alias /srv/market/static/;   # Python'a uğramaz
        expires 7d;
    }
    location / {
        proxy_pass http://api;
        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;
        proxy_read_timeout 30s;
    }
}
server { listen 80; server_name market.example.com; return 301 https://$host$request_uri; }

X-Forwarded-* başlıkları önemlidir: uygulama artık isteği Nginx'ten aldığı için gerçek istemci IP'sini ve isteğin HTTPS olup olmadığını bu başlıklardan öğrenir (uvicorn'da --proxy-headers, Flask'ta ProxyFix). Nginx, location bloklarından isteğin yoluyla en uzun eşleşen öneki seçer; /static/logo.png için /static/, /api/siparis için / çalışır. Yapılandırmayı her değişiklikten sonra nginx -t ile denetle, sonra nginx -s reload ile kesintisiz yeniden yükle.

PaaS'a adım adım dağıtım

Platform hizmeti (PaaS) sunucu, Nginx ve sertifika işini senin yerine yapar. Render, Railway ve Fly.io'da akış neredeyse aynıdır:

  1. Depoda çalışan bir Dockerfile olsun; uygulama portu PORT ortam değişkeninden okusun (--bind 0.0.0.0:$PORT). Platform portu kendisi atar.
  2. GitHub deposunu platforma bağla; her main push'unda imaj otomatik inşa edilir.
  3. Yönetilen PostgreSQL ekle; platform DATABASE_URL'i ortam değişkeni olarak verir. Sırları panelden gir, depoya koyma.
  4. Sağlık kontrolü yolunu (/health) tanımla; yeni sürüm sağlıklı olmadan trafik ona geçmez.
  5. Migration'ı yayın öncesi komut olarak ayarla (Render'da pre-deploy command, Fly'da release_command = "alembic upgrade head").
# Fly.io
fly launch            # fly.toml üretir, bölge ve uygulama adı sorar
fly secrets set SECRET_KEY=... DATABASE_URL=...
fly deploy            # imajı inşa eder, sağlık kontrolüyle yayına alır
fly logs              # canlı log

Kubernetes'in temel nesneleri

Kubernetes (k8s), konteynerleri bir makine kümesinde çalıştıran orkestratördür. Ona "ne istediğini" YAML ile söylersin (bildirimsel, declarative), o da gerçek durumu sürekli istenen duruma yaklaştırır: bir pod çökerse yenisini başlatır.

NesneNe yapar?Compose karşılığı
PodBir ya da birkaç konteynerin en küçük çalışma birimi; geçicidirTek konteyner
Deploymentİstenen kopya sayısını (replicas) korur, sürüm geçişini kademeli yaparservices.api + deploy.replicas
ServiceEtiketle seçilen podlara sabit ad ve IP verir, yükü dağıtırServis adıyla erişim
ConfigMapGizli olmayan ayarlar (metin değerler)environment:
SecretParola, anahtar; değerler base64 ile kodlanır (şifreleme değildir).env / secrets
Probeliveness: takıldıysa yeniden başlat; readiness: hazır değilse trafik vermehealthcheck
apiVersion: apps/v1
kind: Deployment
metadata: {name: market-api}
spec:
  replicas: 3
  selector: {matchLabels: {app: market-api}}
  template:
    metadata: {labels: {app: market-api}}
    spec:
      containers:
        - name: api
          image: ghcr.io/anadolu/market-api:1.4.2
          ports: [{containerPort: 8000}]
          envFrom: [{configMapRef: {name: market-ayar}}, {secretRef: {name: market-sir}}]
          resources: {requests: {cpu: 100m, memory: 128Mi}, limits: {memory: 256Mi}}
          readinessProbe: {httpGet: {path: /ready, port: 8000}, periodSeconds: 5}
          livenessProbe: {httpGet: {path: /health, port: 8000}, initialDelaySeconds: 10}
---
apiVersion: v1
kind: Service
metadata: {name: market-api}
spec:
  selector: {app: market-api}      # template.labels ile AYNI olmalı
  ports: [{port: 80, targetPort: 8000}]
kubectl apply -f k8s/                 # istenen durumu gönder
kubectl get pods -l app=market-api     # durum
kubectl logs deploy/market-api -f      # log
kubectl rollout status deploy/market-api
kubectl rollout undo deploy/market-api # bir önceki sürüme dön
Kendi bilgisayarında deneDocker Desktop'ta Kubernetes'i açabilir ya da kind veya minikube ile yerel küme kurabilirsin. Tarayıcıda küme yok; ama manifestler sadece YAML'dır ve en sık hatalar (seçici uyuşmazlığı, eksik probe, latest etiketi) Python ile yakalanabilir:
import yaml
metin = """
apiVersion: apps/v1
kind: Deployment
metadata: {name: market-api}
spec:
  replicas: 2
  selector: {matchLabels: {app: market-api}}
  template:
    metadata: {labels: {app: market-api, katman: web}}
    spec:
      containers: [{name: api, image: "market-api:latest"}]
---
apiVersion: v1
kind: Service
metadata: {name: market-api}
spec: {selector: {app: market-apı}, ports: [{port: 80, targetPort: 8000}]}
"""
belgeler = list(yaml.safe_load_all(metin))
dep = next(b for b in belgeler if b["kind"] == "Deployment")
etiketler = dep["spec"]["template"]["metadata"]["labels"]
for b in belgeler:
    if b["kind"] == "Service":
        secici = b["spec"]["selector"]
        uyar = all(etiketler.get(k) == v for k, v in secici.items())
        print(b["metadata"]["name"], "podları buluyor mu?", uyar)
for c in dep["spec"]["template"]["spec"]["containers"]:
    print(c["name"], "etiket:", c["image"].rpartition(":")[2], "| probe var mı?", "readinessProbe" in c)

Tek harflik bir fark (market-apı) Service'in hiçbir podu bulamamasına yol açar; kubectl hata vermez, istekler sadece yanıtsız kalır. Bu tür denetimler CI'da kubeconform ve kube-linter gibi araçlarla yapılır.

Gerçek işteÇoğu ekip Kubernetes'e doğrudan değil, Helm şablonları ya da Kustomize ile ortam başına (staging, prod) farklı değerler vererek dağıtım yapar. Yeni başlayan biri için önemli olan YAML'ı okuyabilmek, probe ve kaynak sınırlarını anlamak ve kubectl get/describe/logs ile sorunu bulabilmektir.
Uzmanlaş

PID 1 ve düzgün kapanma

Konteynerde CMD ile başlayan süreç PID 1 olur ve docker stop ona SIGTERM gönderir. CMD python app.py (shell biçimi) önce /bin/sh başlatır ve sinyal Python'a ulaşmayabilir; 10 saniye sonra SIGKILL gelir, yarım kalan istekler kopar. JSON biçimi (CMD ["python", "app.py"]) kullan ve Python'da signal.signal(signal.SIGTERM, ...) ile bağlantı havuzunu kapat.

İmaj boyutu ve güvenlik

python:3.12 yaklaşık 1 GB, python:3.12-slim çok daha küçüktür; alpine daha da küçük ama musl libc yüzünden bazı tekerlek (wheel) paketleri derleme ister. Trivy ya da Docker Scout ile imajı bilinen açıklara karşı tara; temel imajı düzenli güncelle. Etiket yerine özet (digest: python:3.12-slim@sha256:...) ile sabitlemek tam tekrarlanabilirlik sağlar.

Yapılandırmayı doğrulamak

Büyük projelerde ayar sınıfı elle yazılmaz: pydantic-settings ortam değişkenlerini tip ipuçlarına (type hint) göre çevirir ve eksikse açılışta hata verir. Mantık bu dersteki Ayarlar.ortamdan ile aynıdır: hızlı başarısız ol (fail fast), yanlış yapılandırmayla asla istek kabul etme.

Alıştırmalara geç (31)