Docker Compose healthcheck doğru nasıl yazılır?
Docker Compose healthcheck değerlendirmesini, depends_on neden hazır olmayı beklemez sorusunu ve Postgres ile uygulama için çalışan readiness kontrollerini açıklamaktadır.
Docker Compose healthcheck gerçekte ne yapar
Docker Compose healthcheck, Docker'ın zamanlayıcıyla container içinde çalıştırdığı tek bir komuttur. Docker loglarınızı okumaz, portunuzu izlemez veya işlem listenizi incelemez. Komutu çalıştırır, çıkış kodunu okur ve container üzerinde tek bir durum saklar: starting, healthy veya unhealthy. 0 çıkış kodu sağlıklı anlamına gelir. Diğer tüm çıkış kodları sağlıksız anlamına gelir. 2 çıkış kodu Docker tarafından ayrılmıştır; bu nedenle bu kodu kasıtlı olarak döndürmeyin.
Mekanizma tamamen bundan ibarettir. Healthcheck sorunlarının neredeyse tamamı aynı nedenden kaynaklanır: Yazdığınız komut, sormak istediğiniz sorudan farklı bir soruyu yanıtlar. Bu kılavuzda VPS üzerinde compose dosyası yazma konusunu zaten bildiğiniz varsayılır ve stack'in yanlış sırada başlatıldığı noktadan devam edilir.
services:
api:
image: ghcr.io/example/api:1.4.0
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30stest değeri iki yararlı biçimde kullanılabilir. CMD ile başlayan bir liste, komutu shell olmadan doğrudan çalıştırır. Bu nedenle pipe karakterleri, && ve değişken genişletme çalışmaz. CMD-SHELL ile başlayan bir liste, geri kalanını container içinde /bin/sh -c komutuna tek bir dize olarak iletir. Kontrolün shell sözdizimine ihtiyaç duyduğu durumlarda kullanılacak biçim budur. Düz bir dize CMD-SHELL olarak değerlendirilir. Tam olarak ["NONE"] öğesinden oluşan bir liste, image'ın Dockerfile üzerinden eklediği healthcheck'i kaldırır.
Kontrol container içinde çalışır. Bu nedenle komutta adı geçen her binary image içinde bulunmalıdır. Önce bunu doğrulayın. curl bulunmayan slim bir image, uygulama logunda hiç görünmeyen bir nedenle sürekli sağlıksız durumda olan bir container oluşturur. Elle test edin:
docker compose exec api curl --versionEksik bir binary OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown ile yanıt verir. Alpine tabanlı image'larda bunun yerine genellikle BusyBox wget bulunur. Bu nedenle kontrol şu hale gelir: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].
interval, retries ve start_period birlikte nasıl çalışır
Zamanlamayı beş ayar denetler. Bu ayarların varsayılan değerleri Compose'dan değil, Docker Engine'den gelir.
interval: container start period süresini geçtikten sonra iki denetim arasındaki süre. Varsayılan değer 30s.timeout: Docker işlemi sonlandırıp bu çalıştırmayı başarısız saymadan önce tek bir denetimin alabileceği en uzun süre. Varsayılan değer 30s.retries: durumununhealthyolarak değişmesi için gereken ardışık başarısızlık sayısı. Varsayılan değer 3.start_period: container başlatıldıktan sonraki tolerans süresi. Varsayılan değer 0s.start_interval: start period sırasında denetimin ne sıklıkla çalıştırılacağı. Varsayılan değer 5s'dir ve Docker Engine 25.0 veya daha yeni bir sürüm gerekir.
Önemli kural şudur: start period sırasında başarısız bir denetim retries değerine dahil edilmez ve container starting durumunda kalır. Denetim ilk kez başarılı olduğunda container healthy durumuna geçer ve kullanılmamış süre olsa bile start period hemen sona erer. Denetim hâlâ başarısızken start period sona ererse normal geri sayım başlar. Container'ın unhealthy olarak işaretlenmesi için arka arkaya retries başarısızlık gerekir.
Bu nedenle container'ın başlatılmasından unhealthy durumuna geçmesine kadar gereken en uzun süre, start_period artı retries ile interval çarpımının ve timeout değerinin toplamıdır. Yukarıdaki dosyadaki değerlerle bu süre 30 artı 5 çarpı 13, yani 95 saniyedir. Dağıtım zaman aşımını ayarlamadan önce bu sayıyı not edin. 60 saniye sonra vazgeçen bir rollout, bu container'ın nihai duruma ulaşmasını hiçbir zaman göremez.
Buradaki yaygın hata, yavaş başlatmayı karşılamak için retries değerini artırmaktır. Bu ayar ilk seferde işe yarar, ancak daha sonra sürekli sorun oluşturur: Başlatılması için 8 yeniden deneme gereken bir hizmet, üretimde bir sorun fark edilmeden önce arka arkaya 8 başarısızlığa tolerans gösterir. Bunun yerine start_period kullanın. Bu ayar yalnızca ilk başarıdan önce uygulanır.
depends_on tek başına neden hiçbir şeyi garanti etmez
depends_on kısa biçimi, karışıklığın büyük bölümünün kaynağıdır.
api:
depends_on:
- dbBunun anlamı tektir: api container'ından önce db container'ını başlat. Compose, container'ın oluşturulup başlatılmasını bekler. PostgreSQL'in ilk başlatma işlemini tamamlamasını veya 5432 portunun bağlantı kabul etmesini beklemez. Uygulamanız yaklaşık bir saniye sonra başlar, henüz hiçbir işlemin dinlemediği bir porta bağlanır ve çıkar. Günlükte Connection refused veya sunucu çalışır durumda ancak hâlâ kurtarılıyorsa FATAL: the database system is starting up görürsünüz.
Uzun biçim, gerçekte ihtiyaç duyulan biçimdir:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition üç değer alır. service_started, kısa biçimle aynıdır. service_healthy, bağımlılık bir healthcheck tanımlayana kadar bağlı hizmetin başlatılmasını engeller. Bu değer yalnızca bağımlılık compose dosyasında veya kendi image'ında bir healthcheck tanımlıyorsa anlamlıdır. service_completed_successfully, veritabanı migration işlemi gibi tek seferlik bir container'ın 0 durumuyla çıkmasını bekler.
condition yanında iki ek alan bulunur. restart: true, bağımlılık hizmetini güncelledikten sonra bu hizmetin yeniden başlatılmasını Compose'a bildirir. required: false, eksik bağımlılığı hatadan uyarıya dönüştürür.
Şimdi sorunlara yol açan sınıra bakalım. Bu koşullar stack başlatılırken değerlendirilir. Bunlar başlatma sıralamasını belirler; denetim kuralı değildir. Veritabanı sabah 3'te yeniden başlatılırsa service_healthy yeniden değerlendirilmez ve koşulu tekrar sağlamak için uygulamanız yeniden başlatılmaz. Uygulama kodunun bağlantıyı kendisinin yeniden kurması gerekir. docker compose up --no-deps api tasarım gereği bu mekanizmanın tamamını atlar. Container'ı doğrudan docker start ile başlatmak da aynı sonucu doğurur.
Bir işlemin varlığını değil, hazır olma durumunu test eden bir kontrol yazma
pgrep nginx gibi bir kontrol, işlem tablosu girdisinin bulunduğunu kanıtlar. Hizmetin isteği yanıtlayıp yanıtlayabildiği hakkında hiçbir şey göstermez. Bir web uygulaması, veritabanı havuzu durduktan sonra da dinleme soketini uzun süre açık tutabilir ve işlem kontrolü kesinti boyunca başarılı görünmeye devam eder.
Konteynerin var olma amacına uygun işi yapmasını sağlayın:
- Bir HTTP hizmeti için gerçek bir uç noktaya istek gönderin.
curl -fsS,-fnedeniyle 400 veya üzerindeki tüm durum kodlarında sıfırdan farklı bir değer döndürür. Bu nedenle bozuk bir uygulamadan gelen 500 yanıtı başarısız bir kontrol olarak değerlendirilir. - PostgreSQL için, sunucu bağlantıları kabul ediyorsa 0, bağlantıları reddediyorsa 1, hiç yanıt vermiyorsa 2 ve geçirilen parametreler hatalıysa 3 döndüren
pg_isreadykullanın. - Redis için
redis-cli pingkullanın. Bu komutPONGçıktısını verir ve 0 döndürür. - MariaDB için resmi image bir
healthcheck.shbetiği içerir.healthcheck.sh --connect --innodb_initialized, bakımını yapanların belgelediği biçimdir.
pg_isready hakkında bilinmesi gereken bir tuzak vardır. Boş bir veri diziniyle yapılan ilk başlatmada resmi postgres image, başlatma işlemini yalnızca Unix soketini dinleyen geçici bir sunucu üzerinde yürütür. Ana bilgisayar bağıtı olmadan kullanılan pg_isready bu soketi kullanır. Bu nedenle TCP 5432 portu uygulamanız için hâlâ kapalıyken “bağlantıları kabul ediyor” yanıtı verebilir. Kontrolü açıkça TCP'ye yönlendirin. Sorun ortadan kalkar, çünkü geçici sunucu TCP üzerinden yanıt vermez.
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30sÇift dolar işaretleri yazım hatası değildir. Compose, dosyayı okurken $VAR ifadesini kendisi genişletir. Bu işlem, ana makinenizin ortamındaki bir değeri kontrole sabitler. $$ ifadesi bunu tek bir $ işaretine dönüştürerek kaçırır. Böylece konteyner içindeki kabuk, değeri konteynerin kendi ortamına göre genişletir.
Doğru sırada başlayan bir postgres ve uygulama yığını
services:
db:
image: postgres:17.5
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
api:
image: ghcr.io/example/api:1.4.0
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
volumes:
pgdata:Yığını başlatın ve durumların değişimini izleyin:
docker compose up -d
docker compose psSTATUS sütununda köşeli parantez içinde sağlık durumu gösterilir. Sağlıklı bir çiftin her iki satırında da Up 41 seconds (healthy) değeri bulunur. Veritabanı hâlâ başlatılıyorsa db değeri Up 4 seconds (health: starting) olarak görünür ve api listede yer almaz; çünkü Compose bunu henüz oluşturmadı.
Bir denetimin neden başarılı veya başarısız olduğunu görmek için sağlık günlüğünü okuyun:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker son birkaç sonucu saklar. Her sonuçta başlangıç zamanı, bitiş zamanı, bir ExitCode ve komutun Output değeri bulunur. Saklanan çıktı kısaltılır. Bu nedenle büyük bir sayfa gövdesi yazdıran denetim, günlükte kullanışsız bir kayıt oluşturur. Denetimleri sessiz tutun.
Bir container sağlıksız olduğunda Docker ne yapar
Hiçbir şey. İnsanları en çok şaşırtan yanıt budur.
Tek bir host üzerindeki Docker Engine, sağlıksız bir container'ı yeniden başlatmaz. restart: unless-stopped policy'si ana process'in sonlanmasına tepki verir; sağlıksız bir container ise sonlanmamıştır. Compose container'ı olduğu gibi bırakırken container bir hafta boyunca unhealthy durumunda kalabilir. Swarm mode sağlıksız task'ları değiştirir, ancak tek bir sunucudaki sıradan bir Compose stack'i bunu yapmaz.
Bunun iki geçerli seçeneği vardır. Process, bozulduğunu bildiğinde sonlansın; böylece restart policy'sinin işlem yapacağı bir durum oluşur. Ya da durumu dışarıdan izleyip bu durum için alarm üretin. bir Uptime Kuma monitor'ını healthcheck'in çağırdığı endpoint'e yönlendirmek, bozuk bir bağımlılığın her iki yerde de görünmesini sağlar. Böylece bunu bir kullanıcıdan değil, monitor'dan öğrenirsiniz. Traffic uygulamaya bir Traefik reverse proxy üzerinden ulaşıyorsa proxy'nin backend'e ilişkin kendi görünümünün Docker health state'inden ayrı olduğunu unutmayın. Bu nedenle biri diğerinin yerini tutmaz.
Hiçbir zaman sağlıklı duruma geçmeyen bir denetimde hata ayıklama
Komutu aynı container içinde kendiniz tam olarak çalıştırın ve çıkış koduna bakın:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"Container hâlâ sağlıksız olduğunu bildirirken burada exit=0 görülmesi, compose test değerinizin az önce yazdığınızdan farklı olduğu anlamına gelir. Bunun yaygın nedeni, kabuk söz diziminin gerektiği yerde CMD kullanılmasıdır.
Geri kalan sorunların çoğuna iki hata neden olur. İlki yanlış porttur. Healthcheck container içinde çalışır. Bu nedenle yayımlanan host portunu değil, container portunu kullanmalıdır. ports: - "8080:3000" ile uygulama 3000 portunu dinler. Denetimin http://localhost:8080 kullanması, site tarayıcıda sorunsuz çalışsa bile sürekli başarısız olmasına neden olur. İkincisi yanlış hosttur. Denetim içinde localhost aynı container'ı belirtir. Bu, container'ın kendisini denetlemek için doğrudur. Başka bir container'ı denetlemek için yanlıştır. Bu durumda örneğin db gibi service name kullanılmalıdır.
Son bir durum ayrıca adlandırılmalıdır: Healthcheck başarılı olurken kullanıcılar hata görür. Bu durum, endpoint gerçek bir işlem yapmadan statik bir 200 döndürdüğünde ortaya çıkar. Database'i hiç sorgulamayan bir readiness endpoint'i, database'in kullanılamaz duruma geldiğini bildiremez. Endpoint'in bir adet basit gerçek sorgu çalıştırması sağlanmalıdır.
FAQ
Veritabanı sağlıklı olarak belirtilmesine rağmen uygulamam neden hâlâ bağlanamıyor?
Bunun nedeni, condition: service_healthy değerinin yığın başlatılırken yalnızca bir kez değerlendirilmesidir. Sonrasında herhangi bir süreci izlemez. Veritabanı container'ı daha sonra yeniden başlatılırsa Compose, koşulu yeniden karşılamak için uygulamanızı yeniden başlatmaz. Bu nedenle uygulama kodunun kendi yeniden bağlanma ve yeniden deneme mantığı olmalıdır. docker start veya docker compose up --no-deps ile tek bir container başlatıldığında da bu koşul hiçbir işlem yapmaz.
Image zaten bir healthcheck tanımlıyorsa ayrıca healthcheck gerekir mi?
Genellikle gerekmez. Mevcut healthcheck'i geçersiz kılmak çoğu zaman geriye gidiştir, çünkü image'ı yöneten kişi ilgili yazılım için hazır olma durumunun ne anlama geldiğini bilir. Kendi healthcheck'inizi yalnızca image'daki denetim kurulumunuz için yanlışsa ekleyin. Örneğin denetim, taşımış olduğunuz bir portu kontrol ediyorsa kendi denetiminizi ekleyebilirsiniz. Image healthcheck'ini devre dışı bırakmak için hizmet üzerinde test: ["NONE"] veya disable: true ayarlayın.
Healthcheck için curl mı, wget mi kullanılmalı?
Image'da zaten bulunanı kullanın ve buna güvenmeden önce docker compose exec <service> curl --version ile doğrulayın. Debian tabanlı birçok image'da bu araçların hiçbiri bulunmaz. Alpine tabanlı image'larda BusyBox wget bulunur. Yazılım kendi istemcisini sağlıyorsa yalnızca healthcheck çalıştırmak için image'a paket eklemeyin. Örneğin pg_isready veya redis-cli kullanın.
Sağlıksız bir container otomatik olarak yeniden başlatılır mı?
Tek bir host üzerinde Docker Engine tarafından yeniden başlatılmaz. Yeniden başlatma ilkeleri health durumuna değil, sürecin sonlanmasına tepki verir. Bu nedenle sağlıksız bir container, başka bir işlem devreye girene kadar çalışır durumda ve bozuk kalır. Hata algıladığında sürecin sonlanmasını sağlayın veya durumu izleyip uyarı üreten harici bir izleyici çalıştırın.
start_period ne kadar olmalıdır?
Ölçtüğünüz en yavaş geçerli ilk başlatma süresi kadar uzun ve buna ek bir pay içerecek şekilde ayarlanmalıdır. İlk başlatma, sonraki her başlatmadan çok daha yavaş olduğundan, boş bir volume üzerinde docker compose up ile süreyi ölçün. Çok uzun bir start period yalnızca ilk unhealthy kararını geciktirir. Çok yüksek retry değerleri, container'ın tüm yaşam döngüsü boyunca denetimi zayıflatır. Bu daha ciddi bir hatadır.