Docker Compose’da Başlatma Sırası Yönetimi: depends_on ve Condition Kullanımı

Docker Compose ile çalışırken en çok karşılaşılan sorunlardan biri şu: Uygulamanı başlatıyorsun, her şey ayağa kalkıyor gibi görünüyor ama backend servisi veritabanına bağlanamadığı için hata verip duruyor. Ya da bir message broker tam hazır olmadan consumer servisi başlıyor ve mesajları kaçırıyor. Bu tür problemlerin köküne indiğinde hep aynı şeyi görüyorsun: başlatma sırası yönetimi eksik veya yanlış yapılandırılmış.

Docker Compose’un depends_on direktifi ve condition parametresi bu sorunu çözmek için var. Ama çoğu insan bu mekanizmayı yüzeysel kullanıyor, asıl gücünü keşfetmiyor. Bu yazıda konuyu derinlemesine inceleyeceğiz.

Temel Sorun: Konteyner Başladı, Servis Hazır Değil

Önce bir şeyi netleştirelim. Docker Compose’da bir konteyner “başladı” demek, içindeki servisin kullanıma hazır olduğu anlamına gelmiyor. PostgreSQL konteyneri ayağa kalktığında, veritabanı sürecinin bağlantı kabul etmesi genellikle birkaç saniye daha alır. Redis başladığında AOF dosyasını okuyabilir. Elasticsearch cluster’ının hazır hale gelmesi bazen 30-40 saniye sürebilir.

İşte tam bu noktada naive bir depends_on kullanımı seni yanıltır:

# YANLIS KULLANIM - Sadece konteyner başlamasını bekler, servis hazırlığını değil
services:
  web:
    image: myapp:latest
    depends_on:
      - db
  db:
    image: postgres:15

Bu yapılandırmada web servisi, db konteyneri başlar başlamaz ayağa kalkmaya çalışır. PostgreSQL henüz bağlantı kabul etmeye hazır değilken uygulaman “Connection refused” hatası alır ve çöker. Docker Compose bunu otomatik olarak yeniden başlatmaz (restart policy yoksa).

depends_on ve condition Mekanizması

Docker Compose v2 ile birlikte depends_on direktifi condition parametresi alabilir hale geldi. Bu sayede konteyner başlatma sırası çok daha hassas kontrol edilebilir.

Üç temel condition tipi var:

  • service_started: Konteyner başladığında devam et (eski davranış, varsayılan)
  • service_healthy: Healthcheck geçene kadar bekle (önerilen yaklaşım)
  • service_completed_successfully: Konteyner sıfır çıkış kodu ile bitene kadar bekle (init job’lar için)

Şimdi bunları tek tek inceleyelim.

service_started Condition

En basit kullanım senaryosu. Sadece “bu konteyner başladıktan sonra şunu başlat” demek istiyorsun.

services:
  nginx:
    image: nginx:alpine
    depends_on:
      backend:
        condition: service_started
    ports:
      - "80:80"

  backend:
    image: mybackend:latest
    expose:
      - "8080"

Bu yaklaşım çok az sayıda senaryoda yeterli. Mesela nginx başlamadan önce backend konteynerinin var olması gerekiyorsa ve nginx kendi içinde bağlantı yönetimini halledebiliyorsa (upstream down olduğunda retry yapıyorsa) işe yarar. Ama çoğu uygulama bu kadar esnek değil.

service_healthy Condition: Gerçek Güç

Bu condition, bir servisin gerçekten “hazır” olduğunu doğrulamak için healthcheck mekanizmasıyla birlikte çalışır. Kullanabilmek için bağımlı olduğun servise bir healthcheck tanımlamalısın.

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

  backend:
    image: mybackend:latest
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: "postgresql://appuser:secretpassword@postgres:5432/myapp"

Buradaki healthcheck parametrelerini açalım:

  • test: Çalıştırılacak kontrol komutu. CMD-SHELL ile shell üzerinden çalıştırılır
  • interval: Her kontrol arasındaki süre (varsayılan 30s)
  • timeout: Komutun zaman aşımı süresi
  • retries: Kaç kez başarısız olursa “unhealthy” sayılsın
  • start_period: Konteyner başladıktan sonra healthcheck başlamadan önce bekleme süresi

start_period parametresi özellikle önemli. Elasticsearch gibi uzun başlangıç süresine sahip servisler için bu değeri yüksek tutmak gerekiyor, aksi halde servis initialize olmadan “unhealthy” sayılıp bağımlı servisler hiç başlamıyor.

Gerçek Dünya Senaryosu 1: Web Uygulaması Stack

Bir Node.js uygulaması, PostgreSQL veritabanı, Redis cache ve Nginx reverse proxy içeren tipik bir stack düşünelim:

version: "3.9"

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ${DB_NAME:-production}
      POSTGRES_USER: ${DB_USER:-appuser}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-appuser} -d ${DB_NAME:-production}"]
      interval: 5s
      timeout: 3s
      retries: 15
      start_period: 15s
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s
    volumes:
      - redis_data:/data
    restart: unless-stopped

  backend:
    image: myapp-backend:${APP_VERSION:-latest}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DATABASE_URL: "postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}"
      REDIS_URL: "redis://:${REDIS_PASSWORD}@redis:6379"
      NODE_ENV: production
    expose:
      - "3000"
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    depends_on:
      backend:
        condition: service_started
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/ssl/certs:ro
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Bu yapılandırmada başlatma sırası şöyle işliyor: Önce postgres ve redis paralel olarak başlıyor. Her ikisi de healthy durumuna geçtiğinde backend başlıyor. Backend konteyneri başladığında nginx devreye giriyor.

service_completed_successfully Condition: Init Job’lar

Bu condition özellikle veritabanı migration’ları veya tek seferlik kurulum görevleri için biçilmiş kaftan.

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

  db_migrate:
    image: myapp-backend:latest
    command: ["npm", "run", "migrate"]
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: "postgresql://appuser:${DB_PASSWORD}@postgres:5432/myapp"
    restart: "no"

  backend:
    image: myapp-backend:latest
    depends_on:
      db_migrate:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: "postgresql://appuser:${DB_PASSWORD}@postgres:5432/myapp"
    ports:
      - "3000:3000"
    restart: unless-stopped

Burada kritik bir detay var: db_migrate servisi için restart: "no" kullanıyoruz. Aksi halde Compose bu servisi sürekli yeniden başlatmaya çalışır. Migration job başarıyla tamamlandıktan sonra backend devreye giriyor.

Gerçek Dünya Senaryosu 2: Microservice Ortamı

Daha karmaşık bir örnek: Bir event-driven microservice mimarisi. RabbitMQ, birden fazla servis ve bir API gateway.

version: "3.9"

services:
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-admin}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 30s
    ports:
      - "15672:15672"
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq

  queue_setup:
    image: myapp-tools:latest
    command: ["python", "setup_queues.py"]
    depends_on:
      rabbitmq:
        condition: service_healthy
    environment:
      RABBITMQ_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq:5672"
    restart: "no"

  order_service:
    image: order-service:latest
    depends_on:
      rabbitmq:
        condition: service_healthy
      queue_setup:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
    environment:
      RABBITMQ_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq:5672"
      DATABASE_URL: "postgresql://orderuser:${ORDER_DB_PASS}@postgres:5432/orders"

  notification_service:
    image: notification-service:latest
    depends_on:
      rabbitmq:
        condition: service_healthy
      queue_setup:
        condition: service_completed_successfully
    environment:
      RABBITMQ_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq:5672"
      SMTP_HOST: ${SMTP_HOST}

  api_gateway:
    image: api-gateway:latest
    depends_on:
      order_service:
        condition: service_started
      notification_service:
        condition: service_started
    ports:
      - "8080:8080"

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: orderuser
      POSTGRES_PASSWORD: ${ORDER_DB_PASS}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U orderuser -d orders"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  rabbitmq_data:
  pgdata:

Bu yapıda queue_setup servisi, RabbitMQ üzerinde gerekli exchange ve queue’ları oluşturuyor. Sadece bu tamamlandıktan sonra consumer servisler başlıyor.

Healthcheck Yazarken Dikkat Edilmesi Gerekenler

İyi bir healthcheck yazmak düşündüğünden biraz daha nüanslı.

# PostgreSQL için - Sadece sürecin çalışmasını değil, bağlantı kabul etmesini kontrol et
pg_isready -U username -d dbname -h localhost

# MySQL/MariaDB için
mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD}

# Redis için (şifre varsa)
redis-cli -a ${REDIS_PASSWORD} ping

# MongoDB için
mongosh --eval "db.adminCommand('ping')" --quiet

# HTTP servisi için - HTTP status kodu kontrol
curl -f http://localhost:8080/health || exit 1

# Daha detaylı HTTP kontrol - Response body da kontrol et
curl -sf http://localhost:8080/health | grep -q '"status":"ok"' || exit 1

Healthcheck yazarken kaçınılması gereken bazı yaygın hatalar var:

  • Sadece process varlığını kontrol etme: pgrep postgres veya ps aux | grep redis gibi kontroller yanıltıcı. Process çalışıyor olabilir ama servis henüz request kabul etmiyor olabilir.
  • Çok agresif interval: 1-2 saniyelik interval hem kaynakları tüketir hem de log’ları kirletir. 5-10 saniye genellikle makul.
  • start_period’ı atlamak: Özellikle JVM tabanlı uygulamalar (Elasticsearch, Kafka) için start_period kritik. Bu süre zarfında başarısız healthcheck’ler retry sayacını etkilemez.

Elasticsearch için Özel Durum

Elasticsearch başlatma sırası yönetiminde en zorlu servislerden biri. Hem single-node hem cluster modda sağlıklı bir healthcheck örneği:

services:
  elasticsearch:
    image: elasticsearch:8.10.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -sf http://localhost:9200/_cluster/health | grep -qE '"status":"(green|yellow)"'"
        ]
      interval: 15s
      timeout: 10s
      retries: 20
      start_period: 60s
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata:/usr/share/elasticsearch/data

  kibana:
    image: kibana:8.10.0
    depends_on:
      elasticsearch:
        condition: service_healthy
    environment:
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
    ports:
      - "5601:5601"

volumes:
  esdata:

60 saniyelik start_period abartılı gibi görünebilir ama Elasticsearch ilk kez başladığında veya indeks recovery yapıyorsa bu süre çok rahat dolabilir. Bunu kısaltırsan false-unhealthy durumlarıyla karşılaşabilirsin.

Compose’u Komut Satırından Debug Etmek

Başlatma sorunlarını debug ederken birkaç komut hayat kurtarıyor:

# Servislerin durumunu ve healthcheck bilgisini görüntüle
docker compose ps

# Belirli bir servisin healthcheck geçmişini gör
docker inspect myapp-postgres-1 | grep -A 20 '"Health"'

# Servisleri dependency sırasına göre göster
docker compose config --services

# Belirli bir servisi bağımlılıklarıyla birlikte başlat ve logları takip et
docker compose up --wait backend

# Sadece dependency chain'ini başlat, hedef servisi değil
docker compose up -d postgres redis

# Healthcheck loglarını gerçek zamanlı izle
watch -n 2 'docker compose ps'

--wait flag’i özellikle CI/CD pipeline’larında çok işe yarıyor. Bu flag ile docker compose up komutu, tüm servisler healthy durumuna gelene kadar bekliyor ve sonra çıkıyor.

# CI/CD'de kullanım - Tüm servisler healthy olana kadar bekle, timeout 120 saniye
docker compose up -d --wait --wait-timeout 120

# Çıkış kodunu kontrol et
if [ $? -ne 0 ]; then
    echo "Servisler başlatılamadı!"
    docker compose logs
    exit 1
fi

Döngüsel Bağımlılık Problemi

Bazen servisler birbirlerine bağımlı hale gelebilir. Bu durumu Compose algılar ve hata verir, ama bu hatayı görünce ne yapman gerektiğini bilmek lazım.

# BU HATALI - Döngüsel bağımlılık oluşturur
services:
  service_a:
    depends_on:
      service_b:
        condition: service_healthy

  service_b:
    depends_on:
      service_a:
        condition: service_healthy

Eğer gerçekten iki servisin birbirini beklemesi gerekiyorsa (ki bu genellikle mimari problemine işaret eder), çözüm yolu bir ara servis veya sidecar pattern kullanmak, ya da her iki servisin de bağımsız başlayıp kendi içinde retry mekanizması uygulaması.

profiles ile Seçici Başlatma

profiles özelliğiyle birlikte depends_on kullanımında dikkatli olmak gerekiyor. Bir servisin profile’ı aktif değilse, ona bağımlı olan servisler başlatılamaz.

services:
  backend:
    image: myapp:latest
    profiles:
      - production
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:15
    profiles:
      - production
      - development
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

  dev_tools:
    image: pgadmin:latest
    profiles:
      - development
    depends_on:
      postgres:
        condition: service_healthy
# Sadece production profile'ını aktifleştir
docker compose --profile production up -d

# Sadece development profile'ını aktifleştir
docker compose --profile development up -d

Yaygın Hatalar ve Çözümleri

Pratikte sık karşılaştığım sorunları listeleyelim:

  • “dependency failed to start”: Bağımlı servisin healthcheck’i başarısız. docker compose logs ile servis loglarına bak, docker inspect ile healthcheck detaylarını incele.
  • Sonsuz bekleme: retries değeri çok yüksek veya healthcheck komutu daima başarısız oluyordur. retries: 5 ile başla, production’da ihtiyaca göre artır.
  • “no such service” hatası: depends_on içinde yazım hatası. Servis isimlerinde büyük-küçük harf duyarlılığı var.
  • Healthcheck geçiyor ama uygulama hala hazır değil: Healthcheck’in gerçekten uygulama katmanını test etmesi gerekiyor. Sadece port dinleniyor mu kontrol etmek yeterli değil, gerçek bir request at.
  • Windows’ta path problemi: Healthcheck komutlarında Windows line endings (CRLF) sorun yaratabilir, özellikle script dosyaları kullanıyorsan.

Sonuç

depends_on ve condition mekanizması, Docker Compose’un en çok değeri bilinmeyen özelliklerinden biri. Basit görünüyor ama doğru kullanmak gerçekten çevre stabilitesini ciddi ölçüde artırıyor.

Özetlemek gerekirse:

  • service_started: Konteyner sıralamak için yeterli, gerçek hazırlık kontrolü yok
  • service_healthy: Production ortamları için standart olmalı, healthcheck tanımı zorunlu
  • service_completed_successfully: Migration’lar ve init job’lar için ideal

Her yeni Docker Compose stack’i oluştururken şu soruyu sor: “Bu servis gerçekten hazır olmadan diğeri başlarsa ne olur?” Cevap “hata verir veya yanlış davranır” ise service_healthy condition ve düzgün bir healthcheck ekle.

En başta basit bir şey gibi görünen başlatma sırası yönetimi, production ortamında binlerce satır loglama, anlamsız restart döngüleri ve saatler süren debug seanslarını ortadan kaldırıyor. Bu yatırımı yapmaya değer.

Yorum yapın