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.
| Özellik | Sanal makine | Konteyner |
|---|---|---|
| Yalıtım | Ayrı çekirdek, güçlü yalıtım | Ortak çekirdek, süreç düzeyinde yalıtım |
| Açılış süresi | Dakikalar | Saniyeler |
| Boyut | GB düzeyinde | Onlarca–yüzlerce MB |
| Tipik kullanım | Farklı işletim sistemleri, eski sistemler | Mikroservis, API, toplu iş, CI |
| Tanım dosyası | Kurulum betikleri, Packer şablonu | Dockerfile |
İ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
| Komut | Ne yapar | Dikkat |
|---|---|---|
FROM | Temel imajı seçer | Her 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 |
COPY | Dosyaları 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 |
ENV | Ortam değişkeni tanımlar | Sır (şifre, token) asla buraya yazılmaz |
USER | Sonraki komutların kullanıcısı | Varsayılan root'tur; yetkisiz kullanıcıya geç |
EXPOSE | Uygulamanın dinlediği portu belgeler | Portu açmaz; -p açar |
CMD | Konteyner başlarken çalışacak komut | JSON (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"]
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)
yaml.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ı:
| İlke | Anlamı | Python'da karşılığı |
|---|---|---|
| Yapılandırma ortamda | Ortama göre değişen her şey ortam değişkenidir | os.environ, pydantic-settings |
| Bağımlılıklar açık | Her paket sürümüyle beyan edilir | requirements.txt, kilit dosyası |
| Destek servisleri eklenti | Veritabanı bir URL'dir, kod değişmeden değişir | DATABASE_URL |
| Durumsuz süreçler | Oturum, dosya yerel diskte tutulmaz | Redis, nesne deposu |
| Port bağlama | Uygulama kendi portunu dinler | gunicorn/uvicorn -b 0.0.0.0:$PORT |
| Atılabilirlik | Hızlı açılır, SIGTERM'de düzgün kapanır | signal işleyici, bağlantıları kapat |
| Loglar akıştır | Dosyaya değil stdout'a yazılır | logging.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
| Platform | Model | Ne zaman? |
|---|---|---|
| Render, Railway | Git'e bağla, Dockerfile'ı kendisi inşa etsin; yönetilen PostgreSQL | Prototip, küçük ekip, hızlı başlangıç |
| Fly.io | Konteyneri dünyanın farklı bölgelerinde çalıştırır | Kullanı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 konteyner | Değişken trafik, sıfıra ölçeklenme |
| Azure Container Apps / App Service | Yönetilen konteyner ve web uygulaması | Microsoft ekosistemindeki şirketler |
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.İ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:
- Depoda çalışan bir
Dockerfileolsun; uygulama portuPORTortam değişkeninden okusun (--bind 0.0.0.0:$PORT). Platform portu kendisi atar. - GitHub deposunu platforma bağla; her
mainpush'unda imaj otomatik inşa edilir. - Yönetilen PostgreSQL ekle; platform
DATABASE_URL'i ortam değişkeni olarak verir. Sırları panelden gir, depoya koyma. - Sağlık kontrolü yolunu (
/health) tanımla; yeni sürüm sağlıklı olmadan trafik ona geçmez. - 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.
| Nesne | Ne yapar? | Compose karşılığı |
|---|---|---|
| Pod | Bir ya da birkaç konteynerin en küçük çalışma birimi; geçicidir | Tek konteyner |
| Deployment | İstenen kopya sayısını (replicas) korur, sürüm geçişini kademeli yapar | services.api + deploy.replicas |
| Service | Etiketle seçilen podlara sabit ad ve IP verir, yükü dağıtır | Servis adıyla erişim |
| ConfigMap | Gizli olmayan ayarlar (metin değerler) | environment: |
| Secret | Parola, anahtar; değerler base64 ile kodlanır (şifreleme değildir) | .env / secrets |
| Probe | liveness: takıldıysa yeniden başlat; readiness: hazır değilse trafik verme | healthcheck |
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
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.
kubectl get/describe/logs ile sorunu bulabilmektir.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.