Sağlık Kontrolü: Docker Compose’da Health Check Tanımlama

Bir konteynerin “ayakta” olması ile “sağlıklı” olması arasında ciddi bir fark var. Docker, varsayılan olarak bir konteyner prosesinin çalışıp çalışmadığına bakar; ama o prosesin içindeki uygulamanın gerçekten işlevsel olup olmadığını umursamaz. Web sunucunuzun portu dinliyor ama 500 döndürüyor olması, veritabanı bağlantısının kopmuş olması ya da uygulamanın deadlock’a girmiş olması Docker’ın gözünden kaçar. İşte bu yüzden health check mekanizması kritik öneme sahip.

Bu yazıda Docker Compose’da health check tanımlamayı, bağımlılık yönetimiyle entegrasyonunu ve production ortamlarında karşılaşılan gerçek senaryoları ele alacağız.

Health Check Nedir ve Neden Lazım?

Docker’ın health check sistemi, belirli aralıklarla bir komut çalıştırır ve bu komutun çıkış koduna göre konteynerin durumunu belirler. Çıkış kodu 0 ise konteyner healthy, 1 ise unhealthy olarak işaretlenir.

Üç durum var:

  • starting: Konteyner yeni başladı, henüz ilk kontrol yapılmadı
  • healthy: Son kontrol başarıyla geçildi
  • unhealthy: Ardışık kontroller başarısız oldu

Bu mekanizma olmadan şu problemlerle karşılaşırsınız:

  • Uygulama başlatılıyor ama bağımlı servis henüz hazır değil, hata alınıyor
  • Servis çökmüş ama Docker yeniden başlatmıyor çünkü proses hâlâ “çalışıyor”
  • Load balancer trafiği hazır olmayan konteynere yönlendiriyor
  • depends_on beklediği gibi çalışmıyor çünkü servisin “başlamış” olması yetmiyor

Temel Health Check Sözdizimi

Docker Compose’da health check tanımlamak için healthcheck bloğunu servis tanımına ekliyorsunuz:

version: "3.8"

services:
  web:
    image: nginx:alpine
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Parametreleri açıklayalım:

  • test: Çalıştırılacak komut. CMD veya CMD-SHELL formatında yazılır
  • interval: İki kontrol arasındaki süre (varsayılan: 30s)
  • timeout: Komutun tamamlanması için maksimum süre (varsayılan: 30s)
  • retries: Kaç ardışık başarısızlık sonrası unhealthy sayılacağı (varsayılan: 3)
  • start_period: İlk kontrollerin başlamasından önce bekleme süresi (varsayılan: 0s)

test komutunda CMD ile CMD-SHELL farkına dikkat edin:

# CMD: Kabuk yorumlaması olmadan doğrudan çalışır
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]

# CMD-SHELL: /bin/sh -c ile çalışır, pipe ve && kullanabilirsiniz
test: ["CMD-SHELL", "curl -f http://localhost/health && echo 'OK'"]

# Kısa yazım (CMD-SHELL olarak işlenir)
test: "curl -f http://localhost/health"

Web Uygulaması için Health Check

Bir Node.js API’ı düşünelim. Uygulama başlangıçta bağımlılıkları yüklüyor, veritabanı bağlantısı kuruyor ve ancak ondan sonra istekleri kabul etmeye hazır hale geliyor. start_period burada hayat kurtarıcı:

version: "3.8"

services:
  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=postgres
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 60s
    depends_on:
      postgres:
        condition: service_healthy

Burada start_period: 60s ile uygulamaya 60 saniye hazırlanma süresi tanındı. Bu süre içindeki başarısızlıklar retry sayacını artırmaz. Node.js uygulamaları bazen yüzlerce bağımlılık yüklüyor, bu süre gerçekçi bir değer.

PostgreSQL Health Check

Veritabanı health check’i özellikle önemli. PostgreSQL’in çalışıyor olması, bağlantı kabul etmeye hazır olduğu anlamına gelmiyor:

version: "3.8"

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  pgdata:

pg_isready komutu PostgreSQL imajı içinde hazır gelir. Hem kullanıcı hem veritabanı adı belirtmek daha sağlıklı bir kontrol sağlar. Sadece sunucunun ayakta olduğunu değil, o özel veritabanının erişilebilir olduğunu doğrular.

Redis Health Check

Redis için redis-cli ping komutu standart yaklaşım:

version: "3.8"

services:
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass mysecretpass --appendonly yes
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "mysecretpass", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 10s
    ports:
      - "6379:6379"

volumes:
  redisdata:

Şifre korumalı Redis için -a parametresini eklemeyi unutmayın. Şifresiz Redis’te sadece redis-cli ping yeterli.

Gerçek Dünya Senaryosu: Tam Stack Uygulama

Bir e-ticaret uygulamasını ele alalım. Bu uygulamada PostgreSQL, Redis (cache), bir API servisi ve Nginx reverse proxy bulunuyor. Her servisin sağlıklı olması ve bağımlılıkların doğru sırayla başlaması gerekiyor:

version: "3.8"

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped

  api:
    build:
      context: ./api
      dockerfile: Dockerfile.prod
    environment:
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
      - REDIS_URL=redis://redis:6379
      - PORT=3000
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
      interval: 20s
      timeout: 10s
      retries: 3
      start_period: 90s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      replicas: 2

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost/nginx-health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 15s
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Bu yapıda başlangıç sırası şöyle işliyor: önce PostgreSQL ve Redis sağlıklı hale geliyor, ardından API servisi başlıyor ve sağlıklı olunca Nginx devreye giriyor. Nginx sağlıklı olmadan hiçbir dış trafik uygulamaya ulaşamıyor.

Özel Health Check Script Kullanımı

Bazen tek bir komut yetmiyor. Birden fazla koşulu kontrol etmeniz gerekebilir. Bu durumda özel bir script yazıp konteynere dahil etmek daha temiz bir çözüm:

#!/bin/sh
# healthcheck.sh

# HTTP endpoint'i kontrol et
curl -f http://localhost:8080/health > /dev/null 2>&1 || exit 1

# Veritabanı bağlantısını kontrol et
curl -f http://localhost:8080/health/db > /dev/null 2>&1 || exit 1

# Disk kullanımını kontrol et (90% üzeriyse unhealthy)
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 90 ]; then
    echo "Disk usage critical: ${DISK_USAGE}%"
    exit 1
fi

# Bellek kullanımını kontrol et
MEMORY_FREE=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
if [ "$MEMORY_FREE" -lt 104857 ]; then  # 100MB'dan az
    echo "Memory critically low"
    exit 1
fi

exit 0

Dockerfile’a ekleyip Compose’da kullanmak:

# Dockerfile
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Health check script'ini kopyala
COPY healthcheck.sh /usr/local/bin/healthcheck
RUN chmod +x /usr/local/bin/healthcheck

EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yml içinde
services:
  api:
    build: .
    healthcheck:
      test: ["CMD", "/usr/local/bin/healthcheck"]
      interval: 30s
      timeout: 15s
      retries: 3
      start_period: 60s

Health Check Durumunu İzleme

Health check çalışıyor ama nasıl izleyeceksiniz? Birkaç pratik yöntem:

# Tüm konteynerlerin durumunu göster
docker compose ps

# Belirli bir konteynerin health durumunu göster
docker inspect --format='{{json .State.Health}}' konteyner_adi | python3 -m json.tool

# Health check loglarını izle
docker inspect --format='{{range .State.Health.Log}}{{.Start}} - {{.Output}}{{end}}' konteyner_adi

# Sadece health durumunu öğren
docker inspect --format='{{.State.Health.Status}}' konteyner_adi

# Sürekli izleme için watch ile kullan
watch -n 5 'docker compose ps'

Servis sağlıklı olana kadar bekleyen bir script de işinize yarayabilir:

#!/bin/bash
# wait-for-healthy.sh

SERVICE_NAME=$1
MAX_WAIT=${2:-120}
INTERVAL=5
ELAPSED=0

echo "Waiting for $SERVICE_NAME to be healthy..."

while [ $ELAPSED -lt $MAX_WAIT ]; do
    STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$SERVICE_NAME" 2>/dev/null)
    
    if [ "$STATUS" = "healthy" ]; then
        echo "$SERVICE_NAME is healthy after ${ELAPSED}s"
        exit 0
    elif [ "$STATUS" = "unhealthy" ]; then
        echo "$SERVICE_NAME is unhealthy, checking logs..."
        docker inspect --format='{{range .State.Health.Log}}Output: {{.Output}}{{end}}' "$SERVICE_NAME"
        exit 1
    fi
    
    echo "Current status: $STATUS - waiting ${INTERVAL}s... (${ELAPSED}/${MAX_WAIT}s)"
    sleep $INTERVAL
    ELAPSED=$((ELAPSED + INTERVAL))
done

echo "Timeout: $SERVICE_NAME did not become healthy within ${MAX_WAIT}s"
exit 1

MySQL ve MongoDB için Health Check

MySQL için durum biraz farklı. mysqladmin ping yerine gerçek bir sorgu çalıştırmak daha güvenilir:

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: apppass
    healthcheck:
      test: ["CMD-SHELL", "mysql -u appuser -papppass -e 'SELECT 1' myapp || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 60s
    volumes:
      - mysqldata:/var/lib/mysql

  mongodb:
    image: mongo:6
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: mongopass
    healthcheck:
      test: ["CMD-SHELL", "mongosh --eval 'db.adminCommand("ping")' --quiet || exit 1"]
      interval: 15s
      timeout: 10s
      retries: 3
      start_period: 40s
    volumes:
      - mongodata:/data/db

Dockerfile’da Health Check Tanımlama

Health check’i Compose dosyasına değil doğrudan Dockerfile’a da ekleyebilirsiniz. Bu yaklaşım imajı her ortamda aynı davranışı sergilediği için taşınabilirlik açısından avantajlı:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Uygulama portu
EXPOSE 8000

# Health check doğrudan Dockerfile'da
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 
    CMD python -c "import requests; response = requests.get('http://localhost:8000/health'); exit(0 if response.status_code == 200 else 1)"

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

Compose dosyasında bu health check’i devre dışı bırakmak isterseniz:

services:
  api:
    image: myapp:latest
    healthcheck:
      disable: true  # Dockerfile'daki health check'i iptal eder

Sık Yapılan Hatalar

Çok agresif interval ve timeout değerleri: interval: 5s ve timeout: 2s gibi değerler yüksek yük altında sürekli false positive üretiyor. Production’da interval: 15s-30s arası genellikle makul.

start_period’u atlamak: Özellikle Java uygulamaları, Python uygulamaları veya büyük Node.js projeleri başlangıçta ciddi süre alıyor. start_period olmadan konteyner sürekli unhealthy olarak işaretleniyor ve gereksiz restart döngüsüne giriyor.

curl’ü imajda var saymak: Alpine tabanlı minimal imajlarda curl olmayabilir. Bu durumda ya curl yüklüyorsunuz ya wget kullanıyorsunuz ya da dile özgü HTTP istemci kullanıyorsunuz.

# Alpine'da wget alternatifi
healthcheck:
  test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]

# Python ile HTTP kontrolü
healthcheck:
  test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]

# Node.js ile
healthcheck:
  test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1))"]

Sadece port dinlemeyi kontrol etmek: nc -z localhost 3000 komutu portun açık olduğunu kontrol eder ama uygulamanın gerçekten çalışıp çalışmadığını değil. HTTP endpoint kontrolü her zaman daha anlamlı.

Health Check ile Otomatik Yeniden Başlatma

restart politikası ile health check’i birleştirince güçlü bir self-healing mekanizması elde ediyorsunuz:

services:
  worker:
    image: myworker:latest
    healthcheck:
      test: ["CMD-SHELL", "pgrep -f 'python worker.py' || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s
    restart: on-failure
    # ya da:
    # restart: unless-stopped

restart: unless-stopped ile unhealthy olan konteyner otomatik yeniden başlar. Ama dikkat: bu sonsuz döngüye girebilir. Logları izlemek ve --restart-max-attempts gibi limitleri değerlendirmek önemli.

Sonuç

Health check, Docker Compose ile ciddi bir uygulama yönetiyorsanız atlanmaması gereken bir detay. Uygulamalarınıza birkaç satır ekleyerek hem başlangıç sırası problemlerini çözüyor hem de çalışma zamanında oluşan sessiz hataları yakalamış oluyorsunuz.

Özetlemek gerekirse:

  • Her servis için uygun bir health check endpoint’i ya da komutu tanımlayın
  • start_period değerini gerçekçi tutun, uygulamanızın soğuk başlangıç süresini ölçün
  • depends_on ile condition: service_healthy kombinasyonunu kullanın
  • Minimal imajlarda curl yerine alternatif araçları değerlendirin
  • Health check durumlarını izlemek için docker inspect komutlarını alışkanlık haline getirin
  • Production ortamında health check loglarını merkezi log sistemine yönlendirin

Küçük bir yatırım, büyük bir güvenlik ağı. Özellikle gece 2’de telefon çalmadan önce bu kontrolü yapmanızı şiddetle tavsiye ederim.

Yorum yapın