Service Bağımlılıkları: depends_on, Healthcheck Koşulları, Migration Service'i ve Restart
Kısa depends_on yalnız başlatma sırasını verir; bir service'in hazır olmasını beklemek için healthcheck ve condition: service_healthy, tek seferlik bir işin bitmesini beklemek için service_completed_successfully gerekir. SQL Server'a healthcheck yazıyor, şema script'lerini migrate service'ine ve Compose 5.3'ün pre_start init container'ına taşıyor, restart policy'leri ve SqlClient retry logic'ini gerçek kesinti ölçümleriyle deniyoruz.
Sürüm notu: Bu yazıdaki örnekler Docker Compose 5.7.1, Docker Engine 29.9.3, SQL Server 2025 (RTM-CU11), .NET 10 ve Microsoft.Data.SqlClient 7.1.2 ile denenmiştir.
Önce Tahmin Et
Bir önceki yazının compose.yaml'ında API'ye depends_on: [db] ekliyorsun ve volume'ü silip docker compose up -d diyorsun. Compose önce veritabanını, sonra API'yi başlatıyor. API'nin ilk isteği başarılı olur mu? İkinci soru: her şey çalışırken SQL Server process'i çöküyor ve container restart: unless-stopped sayesinde birkaç saniyede geri geliyor. O birkaç saniyede gelen istek ne alır: 500 mü, bekleyip 200 mü?
Kısa depends_on Neyi Garanti Eder?
Denedik. api service'ine kısa formla bağımlılık ekledik ve volume'ü silip başlattık; bir döngü her saniye /api/books'u çağırdı:
api:
...
depends_on:
- db
docker inspect composeseries-db-1 composeseries-api-1 --format "{{.Name}} {{.State.StartedAt}}"
docker compose logs db | grep "ready for client"
/composeseries-db-1 2026-11-14T13:23:45.040984055Z
/composeseries-api-1 2026-11-14T13:23:45.091115846Z
db-1 | 2026-11-14 13:23:51.29 spid43s SQL Server is now ready for client connections. This is an informational message; no user action is required.
API, veritabanı container'ından 50 milisaniye sonra başladı; SQL Server ise bağlantı kabul etmeye 6 saniye sonra hazır oldu. İlk istek 502 aldı (API henüz dinlemiyordu), sonrakiler 500: önce SQL Server'ı bekledi, sonra Login failed for user 'bookstore_app', çünkü şema ve login hâlâ yok. Kısa form yalnız başlatma sırasını verir: Compose db container'ını başlatır ve hemen sıradakine geçer. "Container başladı" ile "içindeki uygulama hizmet vermeye hazır" arasındaki farkı Compose'a söylemenin yolu healthcheck.
SQL Server için Healthcheck
db:
image: mcr.microsoft.com/mssql/server:2025-latest
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD}
volumes:
- db-data:/var/opt/mssql
ports:
- "127.0.0.1:1433:1433"
healthcheck:
# $$, container'a $ olarak ulaşır: değişkeni Compose değil container'daki shell açar
test: SQLCMDPASSWORD="$$MSSQL_SA_PASSWORD" /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -b -Q "SELECT 1" -o /dev/null
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
start_interval: 2s
restart: unless-stopped
SQL Server image'ında Dockerfile'da bir HEALTHCHECK yok, testi biz yazıyoruz. sqlcmd image'ın içinde /opt/mssql-tools18/bin altında (eski image'larda /opt/mssql-tools'tu). -C, sunucunun self-signed sertifikasına güvenmesini söylüyor; mssql-tools18'deki sqlcmd bağlantıyı varsayılan olarak şifrelediği için bu bayrak olmadan test sertifika doğrulamasında kalır. -b bir SQL hatasında sıfırdan farklı exit code döndürür, -Q "SELECT 1" sorguyu çalıştırıp çıkar. Test string olarak yazıldığında Compose onu CMD-SHELL biçiminde, container'daki shell ile çalıştırır.
En kolay gözden kaçan ayrıntı $$. Compose dosyayı okurken $MSSQL_SA_PASSWORD'u kendisi açmaya çalışır; $$ ise "bu dolar işareti bana değil" demek ve container'a tek $ olarak ulaşır. Böylece şifre docker inspect'te görünen healthcheck tanımına yazılmaz, test her çalıştığında container'ın kendi environment variable'ından okunur. Şifreyi -P yerine SQLCMDPASSWORD'la vermek de process listesinde görünmesini engelliyor.
Zamanlama ayarları birlikte okunur. start_period (60 sn) boyunca başarısız testler retries sayacına eklenmez: SQL Server'ın ilk açılışta sistem veritabanlarını hazırlaması zaman alıyor ve bu sürede container'ın unhealthy sayılmasını istemiyoruz. start_interval (2 sn) bu başlangıç süresinde testin ne sıklıkta çalışacağı; Docker Engine 25'ten beri var ve container'ın hazır olduğunu 10 saniyelik interval'ı beklemeden fark ettiriyor. Başlangıçtan sonra test 10 saniyede bir çalışır, 5 saniyede bitmezse başarısız sayılır, art arda 5 başarısızlıkta durum unhealthy olur.
condition: service_healthy ve migrate Service'i
Bir önceki yazıda şema script'lerini elle, docker compose exec ile çalıştırmıştık. O adımı tek seferlik bir service'e çevirdik:
# Tek seferlik: BookstoreDb'yi ve uygulama login'ini oluşturur, 0 ile çıkar
migrate:
image: mcr.microsoft.com/mssql/server:2025-latest
entrypoint: ["/opt/mssql-tools18/bin/sqlcmd"]
command: ["-C", "-S", "db", "-U", "sa", "-b", "-i", "/scripts/01-bookstore.sql", "-i", "/scripts/02-app-login.sql"]
environment:
SQLCMDPASSWORD: ${MSSQL_SA_PASSWORD}
BOOKSTORE_DB_PASSWORD: ${BOOKSTORE_DB_PASSWORD}
volumes:
- ./db:/scripts:ro
depends_on:
db:
condition: service_healthy
api:
...
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
restart: unless-stopped
web:
...
depends_on:
api:
condition: service_healthy
restart: unless-stopped
migrate, SQL Server image'ını yalnız içindeki sqlcmd için kullanıyor: entrypoint sunucuyu başlatmak yerine istemciyi çalıştırıyor, -S db ile ağdaki db service'ine bağlanıyor. Zaten indirilmiş bir image olduğu için ek bir indirme yok. Uzun formdaki üç koşul: service_started kısa formun karşılığı, service_healthy bağımlılığın healthcheck'inin healthy olmasını, service_completed_successfully bağımlılığın 0 exit code'la bitmesini bekler. web'in API'yi beklemesi için ayrıca bir şey yazmadık: Bookstore.Api'nin Dockerfile'ında Docker serisinden kalan bir HEALTHCHECK var ve Compose onu kullanıyor.
Bookstore'un şeması bu seride EF Core (Entity Framework Core; .NET'te tabloları C# class'larıyla eşleyen veri erişim kütüphanesi) migration'ları değil, Docker serisinden gelen idempotent SQL script'leri. EF Core kullanan bir projede aynı migrate service'i migration bundle'ı (efbundle) çalıştırır; bundle'ın kendisini EF Core serisinin "EF Core Migrations: migrations add, database update, Script ve Bundle" yazısı anlatıyor. Kalıp aynı: tek seferlik bir container, sağlıklı veritabanını bekler, işini yapar, 0 ile çıkar; API onu bekler. SQL Server 2025 image'ının başlangıç script'ine baktığımızda bir yol daha gördük: /mssql-server-setup-scripts.d klasörüne konan script'ler, data klasörü boşken, yani yalnız ilk açılışta çalıştırılıyor. İlk kurulum için pratik, ama sonraki sürümlerde gelen şema değişikliklerini uygulamadığı için migration'ın yerini tutmuyor.
docker compose down -v
docker compose up -d
docker compose ps -a --format "table {{.Service}}\t{{.Status}}"
Container composeseries-db-1 Starting
Container composeseries-db-1 Started
Container composeseries-db-1 Waiting
Container composeseries-db-1 Healthy
Container composeseries-migrate-1 Starting
Container composeseries-migrate-1 Started
Container composeseries-migrate-1 Waiting
Container composeseries-migrate-1 Exited
Container composeseries-api-1 Starting
Container composeseries-api-1 Started
Container composeseries-api-1 Waiting
Container composeseries-api-1 Healthy
Container composeseries-web-1 Starting
Container composeseries-web-1 Started
SERVICE STATUS
api Up 5 seconds (healthy)
db Up 15 seconds (healthy)
migrate Exited (0) 5 seconds ago
web Up Less than a second
Zincir artık sırayla ilerliyor ve up -d 16 saniyede döndü; ilk istek 6 kitapla geldi. migrate'in Exited (0) durumu hata değil, işini bitirmiş tek seferlik bir container. ps'te görmek için -a gerekiyor.
Script bir hata verirse ne oluyor? Uygulama şifresini politikaya uymayan bir değerle verdik (shell'deki değişken .env'dekini ezer):
BOOKSTORE_DB_PASSWORD=123 docker compose up -d; echo "exit code: $?"
docker compose logs migrate | tail -2
Container composeseries-migrate-1 Error service "migrate" didn't complete successfully: exit 1
Container composeseries-api-1 not started: condition "service_completed_successfully" of service "migrate" not met
service "migrate" didn't complete successfully: exit 1
exit code: 1
migrate-1 | Msg 33062, Level 16, State 2, Server sql1, Line 5
migrate-1 | Password validation failed. The password does not meet SQL Server password policy requirements because it is too short. The password must be at least 8 characters.
Compose zinciri durdurdu ve sıfırdan farklı exit code döndü; bir CI adımı bunu yakalayabilir. Ama ps'e bakınca daha önemli bir şey görünüyor: api'nin durumu Created. Connection string'deki şifre değiştiği için Compose API container'ını yeniden oluşturmuş, eski container gitmiş, yenisi koşul sağlanmadığı için hiç başlatılmamış. web ayakta ama arkasında API yok. Başarısız bir migration, çalışan bir sistemi de düşürebiliyor; bunu 6. yazıda production deploy akışında tekrar konuşacağız.
Compose 5.3 ile Gelen Yol: pre_start
Ayrı bir migrate service'i çalışıyor ama modelde fazladan bir service, ps -a'da her zaman bir Exited (0) satırı ve her up'ta yeniden koşan bir container demek. Compose 5.3 (Temmuz 2026) Compose Specification'daki pre_start hook'unu uyguladı: service'in container'ı başlamadan önce sırayla çalışan, 0 ile bitmesi gereken tek seferlik container'lar, yani Kubernetes'teki init container'ların karşılığı. Aynı adımı API'nin init container'ı olarak yazdık (labs/compose/02-pre-start.yaml):
services:
api:
depends_on: !override
db:
condition: service_healthy
pre_start:
- image: mcr.microsoft.com/mssql/server:2025-latest
entrypoint: ["/opt/mssql-tools18/bin/sqlcmd"]
command: ["-C", "-S", "db", "-U", "sa", "-b", "-i", "/scripts/01-bookstore.sql", "-i", "/scripts/02-app-login.sql"]
environment:
SQLCMDPASSWORD: ${MSSQL_SA_PASSWORD}
BOOKSTORE_DB_PASSWORD: ${BOOKSTORE_DB_PASSWORD}
volumes:
- ./db:/scripts:ro
docker compose down -v
docker compose -f compose.yaml -f labs/compose/02-pre-start.yaml up -d db api web
curl -s localhost:8080/api/books | jq length
6
Hook'un image'ı belirtilmezse service'in kendi image'ı kullanılır; API'nin chiseled image'ında sqlcmd olmadığı için SQL Server image'ını verdik. Hook, service'in network'lerine bağlanıyor ve service'in container tanımını miras alıyor: docker compose config'te hook'un environment'ında API'nin connection string'i de görünüyor. Davranışı denedik: tanım değişmedikçe sonraki up'larda yeniden çalışmıyor; şifreyi yine 123 yapınca service "api" pre_start[0] exited with code 1 hatası ve hook'un çıktısı doğrudan terminale geldi, başarısız hook container'ı (composeseries-api-pre_start-0) inceleme için bırakıldı ve bir sonraki başarılı up'ta temizlendi. API yine yeniden oluşturulmuş ama başlatılmamış durumdaydı.
Hangisini seçmeli? pre_start modeli sadeleştiriyor ama Compose 5.3 gerektiriyor; Docker Desktop 4.99 Compose 5.7.0 ile geldiği için Windows'ta sorun yok, ama sunucudaki ya da CI runner'daki Compose eski olabilir. Snapshot'ta her Compose 2.x sürümünde çalışan migrate service'ini koruduk, pre_start sürümü labs/ altında. Kubernetes'e geçeceksen iki kalıp da tanıdık gelecek: Kubernetes serisi migration'ı init container ve Job olarak anlatıyor.
Uygulama Tarafında Yine de Retry
depends_on yalnız docker compose up sırasında, başlangıçta geçerli. Çalışırken veritabanı yeniden başlarsa API'ye kimse haber vermiyor. İkinci soruyu ölçtük: labs/compose/02-probe.sh yarım saniyede bir /api/books'u çağırıyor ve bu sırada verilen komutu çalıştırıyor. Önce bir çökme: SQL Server process'ini container'ın içinde kill -9 ile öldürdük.
labs/compose/02-probe.sh 70 'docker compose exec db sh -c "kill -9 \$(ps -o pid= -C sqlservr)"'
docker inspect composeseries-db-1 --format "restarts={{.RestartCount}} health={{.State.Health.Status}}"
1s 200 6703ms
restarts=1 health=healthy
Container exit etti, restart: unless-stopped onu yeniden başlattı (RestartCount=1) ve 70 isteğin hepsi 200 döndü; yalnız biri 6,7 saniye sürdü. SqlClient bağlantıyı açarken Connect Timeout (varsayılan 15 saniye) dolana kadar bekliyor ve sunucu bu sürede geri geldiği için istek başarılı oldu. Aynı deneyi docker compose kill db ile yaptığımızda container yeniden başlamadı: Docker elle durdurulan ya da öldürülen container'a restart policy uygulamıyor. Restart policy çökmelere ve Docker servisinin yeniden başlamasına karşı; senin komutlarına karşı değil.
Şimdi 15 saniyeden uzun bir kesinti: veritabanını durdurup 20 saniye sonra başlattık.
labs/compose/02-probe.sh 50 "docker compose stop db; sleep 20; docker compose start db"
11s 500 14682ms
26s 200 10959ms
api-1 | Microsoft.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. ...
api-1 | Error Number:11001,State:0,Class:20
Bir istek 14,7 saniye bekleyip 500 aldı. Hata numarası ilginç: 11001, "host bulunamadı". Durdurulan container network'ten çıktığı için Docker'ın DNS'i db adını artık çözmüyor. SqlClient'in configurable retry logic'ini (Microsoft.Data.SqlClient 3.0'dan beri var, varsayılan olarak kapalı) bağlantı açma adımına ekledik:
// BookstoreDb'den (SQL Server) kataloğu okur
public sealed class SqlBookSource : IBookSource
{
private readonly string connectionString;
private readonly SqlRetryLogicBaseProvider retryProvider;
public SqlBookSource(string connectionString, ILogger<SqlBookSource> logger)
{
this.connectionString = connectionString;
// Bağlantı açılırken gelen geçici hatalar exponential backoff ile yeniden denenir.
// 11001: container durdurulduğunda ya da yeniden oluşturulurken db service adı çözülemez
retryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(new SqlRetryLogicOption
{
NumberOfTries = 5,
DeltaTime = TimeSpan.FromSeconds(1),
MaxTimeInterval = TimeSpan.FromSeconds(10),
TransientErrors = [.. SqlConfigurableRetryFactory.BaselineTransientErrors, 11001],
});
retryProvider.Retrying += (_, e) => logger.LogWarning(
"SQL connection attempt {Attempt} failed with error {Error}, retrying in {Delay}",
e.RetryCount, (e.Exceptions[^1] as SqlException)?.Number, e.Delay);
}
public async Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken)
{
await using SqlConnection connection = new(connectionString) { RetryLogicProvider = retryProvider };
await connection.OpenAsync(cancellationToken);
...
}
}
builder.Services.AddSingleton<IBookSource>(services =>
new SqlBookSource(connectionString, services.GetRequiredService<ILogger<SqlBookSource>>()));
NumberOfTries ilk deneme dahil toplam deneme sayısı. TransientErrors'a bir liste vermek varsayılan listeyi değiştirir, ona eklemez; SqlClient 7.0'daki BaselineTransientErrors varsayılan 20 hata numarasını verdiği için listeyi kopyalamadan 11001'i ekleyebildik. Aynı kesinti, yeni image'la:
11s 200 26441ms
api-1 | warn: Bookstore.Api.SqlBookSource[0]
api-1 | SQL connection attempt 1 failed with error 11001, retrying in 00:00:01.0130000
api-1 | warn: Bookstore.Api.SqlBookSource[0]
api-1 | SQL connection attempt 2 failed with error 11001, retrying in 00:00:03.1710000
api-1 | warn: Bookstore.Api.SqlBookSource[0]
api-1 | SQL connection attempt 3 failed with error 11001, retrying in 00:00:07.3430000
Hata yok, ama istek 26 saniye sürdü. Retry hatayı gecikmeye çeviriyor; kullanıcının 26 saniye beklemesi her zaman 500'den iyi değil. Sınırı uygulamaya göre seç ve nginx'in proxy_read_timeout'unun (varsayılan 60 sn) retry'nin toplam süresinden uzun olduğundan emin ol. EF Core kullanıyorsan aynı işi EnableRetryOnFailure DbContext seviyesinde yapıyor; retry, timeout ve circuit breaker'ın daha geniş resmi ilerideki External Service Integration ve Resilience serisinde.
Üretimde Dikkat
Tek seferlik service her up'ta yeniden çalışır: API'yi up -d --build api ile yeniden build ettiğimizde Compose bağımlılık olarak migrate'i de yeniden koştu. Script'lerin idempotent olması bu yüzden zorunlu; Docker serisinden gelen script'ler IF ... IS NULL ve MERGE ile yazıldığı için sorun yok. Healthcheck'in kendisi de bir maliyet: her test container'da bir process çalıştırıyor, 2 saniyelik start_interval yalnız başlangıçta geçerli olduğu için kalıcı yük 10 saniyede bir sqlcmd. Healthcheck'i ucuz ve yalnız "bu service istek kabul ediyor mu" sorusuna odaklı tut; dış bağımlılıkları kontrol eden bir healthcheck, veritabanı yavaşladığında bütün API'yi unhealthy gösterir. Liveness ve readiness ayrımını ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Health Check: Liveness, Readiness ve Veritabanı Kontrolü" yazısı anlatıyor.
CI'da ya da bir deploy script'inde docker compose up -d --wait, bütün service'ler healthy (healthcheck'i olmayanlar running) olana kadar bekler ve olmazsa sıfırdan farklı exit code döner; 5. yazıda pipeline adımı olarak kullanacağız.
Sık Düşülen Tuzaklar
- "wait-for-it" ya da
sleep 10içeren entrypoint script'leriyle sırayı garanti ettiğini sanmak: port'un açılması veritabanının hazır olduğu anlamına gelmez ve bu script'ler yalnız ilk başlangıcı kapsar; çalışma sırasındaki kesintiler için uygulamada retry gerekir. - Healthcheck'i olmayan bir image'a
service_healthyvermek: Compose beklemez, hata verir. Redis image'ıyla denedik:dependency failed to start: container composeseries-nohc-cache-1 has no healthcheck configured. - Healthcheck'te
$$yerine$yazmak: değişkeni Compose dosyayı okurken açar ve şifre container tanımına, dolayısıyladocker inspectçıktısına yazılır; değişken host'ta yoksa test boş şifreyle sonsuza kadar başarısız olur. start_period'u kısa tutmak: emülasyonda ya da yavaş bir diskte SQL Server'ın ilk açılışı bir dakikayı bulabilir ve container daha açılmadanunhealthysayılır.- Migration hatasının API'yi de durdurabileceğini unutmak: bağımlı service'in tanımı değiştiyse Compose onu yeniden oluşturur ve koşul sağlanmazsa yenisini başlatmaz.
- Restart policy'nin elle durdurulan container'ı da kaldıracağını sanmak:
docker compose stopya dakillsonrasında container, sen başlatana kadar kapalı kalır.
Küçük Bir Deneme
★☆☆ db'nin healthcheck'inden start_period ve start_interval'ı silip docker compose down -v && time docker compose up -d ile süreyi ölç ve yazıdaki 16 saniyeyle karşılaştır. Aradaki fark hangi ayardan geliyor?
★★☆ api'nin db bağımlılığına restart: true ekle ve docker compose restart db çalıştır. API container'ının StartedAt değeri değişiyor mu? labs/compose/02-probe.sh ile bu sırada kaç isteğin etkilendiğini karşılaştır.
★★★ Retry ayarlarını NumberOfTries = 3 ve MaxTimeInterval = 2s yap ve 20 saniyelik kesinti deneyini tekrarla. Hangi istekler 500 aldı, her biri ne kadar sürdü? Kullanıcı açısından hangisi daha iyi: hızlı bir hata mı, 26 saniyelik bir bekleme mi? Cevabını katalog sayfası ve ödeme adımı için ayrı ayrı düşün.
Bu yazıda iki şifreyi hâlâ .env'den environment variable olarak veriyoruz ve config çıktısında açık görünüyorlar. Bir sonraki yazıda .env interpolation'ı ile env_file'ın farkını, ${VAR:?} ile zorunlu değişkenleri, şifreleri /run/secrets altında dosya olarak veren secrets:'ı ve opsiyonel service'ler için profile'ları görüyoruz.