Docker Compose Healthcheck Yapılandırması ve İpuçları
Docker Compose healthcheck mekanizmasının çalışma prensiplerini öğrenin. depends_on komutunun neden yetersiz kaldığını ve Postgres ile uygulama için hazır olma kontrollerini inceleyin.
Docker Compose healthcheck mekanizması gerçekte ne yapar
Docker Compose healthcheck, Docker'ın bir zamanlayıcıya bağlı olarak container içerisinde çalıştırdığı tek bir komuttur. Docker loglarınızı okumaz, portunuzu izlemez veya süreç listenizi denetlemez. Sadece komutu çalıştırır, çıkış kodunu okur ve container üzerinde tek bir durum kaydeder: 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 numaralı çıkış kodu Docker tarafından rezerve edilmiştir, bu yüzden bu kodu bilerek döndürmeyin.
Mekanizmanın tamamı budur. Neredeyse tüm healthcheck sorunları aynı nedene dayanır: yazdığınız komut, sormak istediğinizden farklı bir soruya yanıt vermektedir. Bu rehber, bir VPS üzerinde compose dosyası yazmayı bildiğinizi varsayar ve stack'in yanlış sırada başladığı noktadan devam eder.
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 kullanışlı biçim alır. CMD ile başlayan bir liste, komutu doğrudan çalıştırır; shell kullanılmaz, bu nedenle pipe işlemleri, && ve değişken genişletme çalışmaz. CMD-SHELL ile başlayan bir liste ise geri kalan kısmı container içindeki /bin/sh -c'ya tek bir metin dizisi olarak iletir; shell söz dizimi gerektiren kontrollerde kullanmanız gereken yöntem budur. Düz bir metin dizisi ise CMD-SHELL olarak işlenir. Tam olarak ["NONE"] içeren bir liste, imajın Dockerfile içerisinde gömülü olarak gelen healthcheck'ini kaldırır.
Kontrol container içerisinde çalıştığı için, belirtilen her ikili dosyanın o imajda mevcut olması gerekir. Öncelikle bunu doğrulayın; çünkü curl içermeyen ince (slim) bir imaj, uygulama loglarında asla görünmeyen bir nedenle kalıcı olarak sağlıksız bir container üretir. Manuel olarak test edin:
docker compose exec api curl --versionEksik bir ikili dosya OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown ile yanıt verir. Alpine tabanlı imajlar genellikle bunun yerine BusyBox wget içerir, bu yüzden kontrol ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"] haline gelir.
interval, retries ve start_period ayarlarının birlikte çalışması
Zamanlamayı beş farklı ayar kontrol eder. Bu ayarların varsayılan değerleri Compose tarafından değil, Docker Engine tarafından belirlenir.
interval: Konteyner start period süresini geçtikten sonra iki kontrol arasında geçen süre. Varsayılan değer 30s.timeout: Docker'ın süreci sonlandırıp başarısız sayması için bir kontrolün ne kadar sürebileceği. 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: Konteyner başladıktan sonra tanınan tolerans süresi. Varsayılan değer 0s.start_interval: Start period sırasında kontrolün ne sıklıkla çalışacağı. Varsayılan değer 5s olup Docker Engine 25.0 veya daha yeni bir sürüm gerektirir.
Önemli kural şudur: Start period süresince başarısız olan bir kontrol retries sayısına dahil edilmez ve konteyner starting durumunda kalır. Kontrol ilk kez başarılı olduğunda, start period süresi henüz dolmamış olsa bile konteyner healthy durumuna geçer ve start period hemen sona erer. Eğer start period süresi dolduğunda kontrol hala başarısız olmaya devam ediyorsa, normal geri sayım başlar ve konteynerin unhealthy olarak işaretlenmesi için retries kadar ardışık başarısızlık gerekir.
Bu durumda, konteynerin başlamasından unhealthy durumuna geçişine kadar geçen en kötü senaryo süresi; start_period artı retries çarpı interval artı timeout formülüyle hesaplanır. Yukarıdaki dosyadaki değerlerle bu süre 30 artı 5 çarpı 13, yani 95 saniyedir. Bir deploy zaman aşımı (timeout) belirlemeden önce bu sayıyı not edin; çünkü 60 saniye sonra pes eden bir dağıtım süreci, bu konteynerin nihai duruma ulaştığını asla göremeyecektir.
Burada yapılan yaygın hata, yavaş başlayan bir servisi kurtarmak için retries değerini artırmaktır. Bu yöntem bir kez işe yarasa da kalıcı bir zafiyet yaratır: Başlatılması için 8 deneme gereken bir servis, artık üretim ortamında herhangi bir uyarı tetiklenmeden önce 8 ardışık başarısızlığı tolere eder hale gelir. Bunun yerine, yalnızca ilk başarılı çalışmaya kadar geçerli olan start_period ayarını kullanın.
Neden depends_on tek başına hiçbir şeyi garanti etmez
depends_on ifadesinin kısa biçimi, kafa karışıklığının ana kaynağıdır.
api:
depends_on:
- dbBu ifade tek bir anlama gelir: db container'ını api container'ından önce başlat. Compose, container'ın oluşturulmasını ve başlatılmasını bekler. PostgreSQL'in ilk kurulumunu tamamlamasını beklemez ve 5432 numaralı portun bağlantı kabul etmesini beklemez. Uygulamanız yaklaşık bir saniye sonra başlar, henüz hiçbir şeyin dinlemediği bir porta bağlanmaya çalışır ve kapanır. Günlük kayıtlarında Connection refused veya sunucu ayakta olmasına rağmen henüz kurtarma aşamasındaysa FATAL: the database system is starting up hatasını görürsünüz.
İnsanların aslında ihtiyaç duyduğu şey uzun biçimdir:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition üç farklı değer alır. service_started, kısa biçimle aynıdır. service_healthy, bağımlı servisi, bağımlılık "healthy" (sağlıklı) durumunu bildirene kadar bekletir; bu durum yalnızca bağımlılık, compose dosyasında veya kendi image'ı içerisinde bir healthcheck tanımladığında anlamlıdır. service_completed_successfully, veritabanı migrasyonu gibi tek seferlik (one-shot) bir container'ın 0 durumuyla çıkış yapmasını bekler.
condition yanında iki ek alan daha bulunur. restart: true, bağımlılık servisi güncellendikten sonra bu servisin yeniden başlatılmasını sağlar. required: false, eksik bir bağımlılığı hata seviyesinden uyarı seviyesine indirger.
Şimdi ise kullanıcıları en çok zorlayan sınırlamaya gelelim. Bu koşullar, stack ayağa kalkarken değerlendirilir. Bunlar başlatma sıralamasıdır, bir denetim kuralı değildir. Eğer veritabanı gece saat üçte yeniden başlarsa, hiçbir mekanizma service_healthy koşulunu tekrar değerlendirmez ve uygulamanızı tekrar çalıştırmak için yeniden başlatmaz. Uygulama kodunuzun bağlantıyı kendi başına yeniden kurması gerekir. docker compose up --no-deps api, tasarımı gereği bu mekanizmayı tamamen atlar; aynı durum docker start ile doğrudan bir container başlatıldığında da geçerlidir.
Sadece sürecin varlığını değil, hazır olma durumunu test eden bir denetim yazın
pgrep nginx gibi bir denetim, yalnızca süreç tablosunda bir girdi olduğunu kanıtlar. Servisin bir isteğe yanıt verip veremeyeceği hakkında hiçbir bilgi sağlamaz. Bir web uygulaması, veritabanı havuzu kapandıktan çok sonra bile dinleme soketini açık tutabilir; bu durumda süreç denetimi, kesinti boyunca başarılı görünmeye devam eder.
Konteynırdan, var olma amacına uygun işi yapmasını isteyin:
- Bir HTTP servisi için gerçek bir uç noktaya istek gönderin.
curl -fsS,-fparametresi nedeniyle 400 ve üzeri her durumda sıfır olmayan bir çıkış kodu üretir; dolayısıyla bozuk bir uygulamadan dönen 500 hatası, başarısız bir denetim olarak kabul edilir. - PostgreSQL için, sunucu bağlantıları kabul ettiğinde 0, reddettiğinde 1, hiç yanıt vermediğinde 2 ve parametreler hatalı olduğunda 3 değerini döndüren
pg_isreadykomutunu kullanın. - Redis için,
PONGçıktısını veren ve 0 ile çıkanredis-cli pingkomutunu kullanın. - MariaDB için, resmi imaj bir
healthcheck.shbetiği ile gelir ve geliştiricilerin dokümante ettiği yöntemhealthcheck.sh --connect --innodb_initializedşeklindedir.
pg_isready kullanımında bilinmesi gereken bir tuzak vardır. Boş bir veri dizini ile ilk kez başlatıldığında, resmi postgres imajı, başlatma işlemini yalnızca Unix soketini dinleyen geçici bir sunucu üzerinde gerçekleştirir. Host argümanı içermeyen pg_isready, bu soketi kullanır; dolayısıyla TCP 5432 portu uygulamanıza henüz kapalıyken bile "bağlantıları kabul ediyor" yanıtını verebilir. Denetimi açıkça TCP adresine yönlendirirseniz bu 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 bir yazım hatası değildir. Compose, dosyayı okurken $VAR ifadesini kendisi genişletir; bu da host ortamınızdaki bir değeri denetimin içine gömer. $$, bu ifadeyi tek bir $ işaretine dönüştürerek kaçış sağlar; böylece konteynır içindeki kabuk, genişletme işlemini konteynırın kendi ortamına göre gerçekleştirir.
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:Sistemi ayağa kaldırın ve durum değişikliklerini izleyin:
docker compose up -d
docker compose psSTATUS sütunu, sağlık durumunu parantez içinde gösterir. Sağlıklı bir çiftin her iki satırında da Up 41 seconds (healthy) yazar. Veritabanı henüz başlatılma aşamasındayken, db değeri Up 4 seconds (health: starting) olarak görünür ve Compose henüz oluşturmadığı için api listede yer almaz.
Bir kontrolün 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, her biri bir başlangıç zamanı, bitiş zamanı, bir ExitCode ve komutun Output değerini içeren son birkaç sonucu saklar. Saklanan çıktı kısaltılmıştır; bu nedenle büyük bir sayfa gövdesini yazdıran bir kontrol, işe yaramaz bir günlük girdisi oluşturur. Kontrolleri sessiz tutun.
Bir container sağlıksız duruma düştüğünde Docker ne yapar
Hiçbir şey. Bu, insanları en çok şaşırtan cevaptır.
Tek bir sunucu üzerindeki Docker Engine, sağlıksız bir container'ı yeniden başlatmaz. restart: unless-stopped politikası, ana sürecin sonlanmasına tepki verir; ancak sağlıksız bir container sonlanmış değildir. Compose onu kendi haline bıraktığında, container bir hafta boyunca unhealthy durumunda kalabilir. Swarm modu sağlıksız görevleri değiştirir, ancak tek bir sunucudaki basit bir Compose yığını bunu yapmaz.
Geriye iki dürüst seçenek kalır. Sürecin bozulduğunu anladığında sonlanmasını sağlayarak yeniden başlatma politikasının harekete geçebileceği bir durum yaratmak. Veya durumu dışarıdan izleyip uyarı almak. Bir Uptime Kuma monitörünü, healthcheck'inizin çağırdığı uç noktaya yönlendirmek, bozuk bir bağımlılığın her iki yerde de görünmesi anlamına gelir; böylece durumdan bir kullanıcıdan önce monitör aracılığıyla haberdar olursunuz. Eğer trafik uygulamaya bir Traefik reverse proxy üzerinden ulaşıyorsa, proxy'nin bir backend'e dair kendi görüşünün Docker sağlık durumundan ayrı olduğunu unutmayın; bu nedenle biri diğerinin yerini tutmaz.
Sağlıklı duruma geçmeyen bir kontrolün hata ayıklaması
Komutu aynı container içerisinde bizzat çalıştırın ve çıkış kodunu inceleyin:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"Container hala sağlıksız durumdayken burada exit=0 almanız, compose test dosyanızın az önce yazdığınızdan farklı olduğu anlamına gelir; bu durum genellikle shell sözdizimi gereken yerde CMD kullanılmasından kaynaklanır.
Geriye kalan sorunların çoğu iki hatadan kaynaklanır. Birincisi yanlış port kullanımıdır. Healthcheck, container içerisinde çalıştığı için container portunu kullanmalıdır; asla host üzerinde yayınlanan portu kullanmamalıdır. ports: - "8080:3000" ile uygulama 3000 portunda dinleme yapıyorsa, http://localhost:8080 adresine yapılan bir kontrol, site tarayıcıda düzgün çalışsa bile sürekli başarısız olur. İkincisi ise yanlış host kullanımıdır. Kontrol içerisinde localhost ifadesi aynı container'ı temsil eder; bu, container'ın kendisini kontrol etmesi için doğrudur ancak başka bir servisi kontrol etmek için yanlıştır. Diğer servisler için servis adını kullanmanız gerekir, örneğin db.
Son bir durum ise özel olarak belirtilmelidir: healthcheck başarılı olur ancak kullanıcılar hata alır. Bu durum, uç noktanın gerçek bir işlem yapmadan statik bir 200 kodu döndürmesiyle gerçekleşir. Veritabanını sorgulamayan bir hazır olma (readiness) uç noktası, veritabanının erişilemez olduğunu size bildiremez. Bu uç noktanın düşük maliyetli, gerçek bir sorgu çalıştırmasını sağlayın.
FAQ
depends_on veritabanının sağlıklı olduğunu belirtmesine rağmen uygulamam neden hala bağlanamıyor?
Çünkü condition: service_healthy yalnızca yığın (stack) başlatıldığında bir kez değerlendirilir. Daha sonrasında herhangi bir denetim yapmaz. Veritabanı container'ı daha sonra yeniden başlatılırsa, Compose koşulu tekrar sağlamak için uygulamanızı yeniden başlatmaz; bu nedenle uygulama kodunuzun kendi yeniden bağlanma ve yeniden deneme mantığına sahip olması gerekir. Ayrıca docker start veya docker compose up --no-deps ile tek bir container başlattığınızda bu koşul hiçbir işlev görmez.
Image zaten bir healthcheck tanımlamışsa, benimkini eklemem gerekir mi?
Genellikle gerekmez ve bunu geçersiz kılmak çoğu zaman bir gerilemedir; çünkü image sorumlusu, ilgili yazılım için "hazır olma" durumunun ne anlama geldiğini bilir. Kendi kontrolünüzü yalnızca image içindeki kontrol kurulumunuz için yanlışsa (örneğin taşıdığınız bir portu sorguluyorsa) ekleyin. Bir image healthcheck'ini devre dışı bırakmak için servis üzerinde test: ["NONE"] veya disable: true ayarını kullanın.
Healthcheck için curl mü yoksa wget mi kullanmalıyım?
Image içinde halihazırda hangisi varsa onu kullanın ve güvenmeden önce docker compose exec <service> curl --version ile doğrulayın. Debian tabanlı birçok image'da ikisi de bulunmaz. Alpine tabanlı image'lar BusyBox wget içerir. Yazılımın kendi istemcisi (örneğin pg_isready veya redis-cli) mevcutsa, sadece healthcheck çalıştırmak için image'a yeni bir paket eklemeyin.
Sağlıksız (unhealthy) bir container otomatik olarak yeniden başlatılır mı?
Tek bir sunucuda Docker Engine tarafından başlatılmaz. Yeniden başlatma ilkeleri (restart policies), sürecin sonlanmasına tepki verir, sağlık durumuna değil. Bu nedenle sağlıksız bir container, başka bir mekanizma müdahale edene kadar çalışmaya devam eder ve bozuk kalır. Ya hatayı tespit ettiğinde sürecin sonlanmasını sağlayın ya da bu durum hakkında uyarı veren harici bir izleme aracı çalıştırın.
start_period ne kadar uzun olmalı?
Ölçtüğünüz en yavaş meşru ilk başlatma süresi kadar ve buna ek bir pay bırakacak kadar uzun olmalıdır. Veritabanının ilk başlatılması sonraki tüm başlatmalardan çok daha yavaş olduğu için, boş bir volume ile docker compose up kullanarak süreyi ölçün. Çok uzun bir başlatma süresi, yalnızca ilk unhealthy kararını geciktirir. Çok yüksek yeniden deneme sayıları ise container'ın tüm yaşam döngüsü boyunca kontrolü zayıflatır ki bu daha kötü bir hatadır.