Docker Compose healthcheck कैसे सही तरीके से सेटअप करें
Docker Compose healthcheck कैसे काम करते हैं और depends_on का सही उपयोग कैसे करें। Postgres और अपनी ऐप के लिए readiness check लिखने का सटीक तरीका यहाँ विस्तार से जानें।
Docker Compose healthcheck वास्तव में क्या करता है
Docker Compose healthcheck एक कमांड है जिसे Docker एक टाइमर पर कंटेनर के अंदर चलाता है। Docker आपके लॉग्स को नहीं पढ़ता, आपके पोर्ट की निगरानी नहीं करता, और न ही आपकी प्रोसेस लिस्ट की जांच करता है। यह कमांड को चलाता है, एग्जिट कोड पढ़ता है, और कंटेनर पर एक सिंगल स्टेट स्टोर करता है: starting, healthy, या unhealthy। एग्जिट कोड 0 का मतलब है healthy। कोई भी अन्य एग्जिट कोड unhealthy का संकेत है, और एग्जिट कोड 2 को Docker द्वारा आरक्षित किया गया है, इसलिए इसे कभी भी जानबूझकर रिटर्न न करें।
यही पूरी कार्यप्रणाली है। लगभग हर healthcheck समस्या एक ही होती है: आपके द्वारा लिखी गई कमांड उस सवाल का जवाब देती है जो आपने नहीं पूछा था। यह गाइड मानती है कि आप पहले से ही जानते हैं कि compose फ़ाइल को VPS पर कैसे लिखें, और यह वहां से शुरू होती है जहाँ स्टैक गलत क्रम में शुरू होता है।
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 वैल्यू दो उपयोगी रूपों में आती है। CMD से शुरू होने वाली लिस्ट कमांड को सीधे चलाती है, बिना किसी शेल के, इसलिए पाइप्स और && और वेरिएबल एक्सपेंशन काम नहीं करते हैं। CMD-SHELL से शुरू होने वाली लिस्ट बाकी हिस्से को एक स्ट्रिंग के रूप में कंटेनर के अंदर /bin/sh -c को पास करती है, जो कि तब आवश्यक होता है जब चेक को शेल सिंटैक्स की आवश्यकता हो। एक साधारण स्ट्रिंग को CMD-SHELL के रूप में माना जाता है। ठीक ["NONE"] की एक लिस्ट उस healthcheck को हटा देती है जिसे इमेज ने अपने Dockerfile के माध्यम से इनबिल्ट किया था।
चेक कंटेनर के अंदर चलता है, इसलिए इसमें नामित प्रत्येक बाइनरी का उस इमेज में मौजूद होना आवश्यक है। पहले इसकी पुष्टि करें, क्योंकि बिना curl वाली एक स्लिम इमेज एक ऐसा कंटेनर बनाती है जो स्थायी रूप से unhealthy रहता है, जिसका कारण एप्लिकेशन लॉग में कभी दिखाई नहीं देता। इसे मैन्युअल रूप से टेस्ट करें:
docker compose exec api curl --versionएक गायब बाइनरी OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown के साथ जवाब देती है। Alpine आधारित इमेज आमतौर पर इसके बजाय BusyBox wget का उपयोग करती हैं, इसलिए चेक ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"] हो जाता है।
interval, retries और start_period कैसे संयोजित होते हैं
पाँच सेटिंग्स टाइमिंग को नियंत्रित करती हैं। इनके डिफ़ॉल्ट मान Docker Engine से आते हैं, Compose से नहीं।
interval: कंटेनर के start period के बाद दो जाँचों के बीच का समय। डिफ़ॉल्ट 30s है।timeout: जाँच का एक रन Docker द्वारा उसे समाप्त करने और उस रन को विफलता के रूप में गिनने से पहले कितना समय ले सकता है। डिफ़ॉल्ट 30s है।retries: स्थिति केunhealthyमें बदलने से पहले कितनी लगातार विफलताओं की आवश्यकता है। डिफ़ॉल्ट 3 है।start_period: कंटेनर शुरू होने के बाद एक ग्रेस विंडो। डिफ़ॉल्ट 0s है।start_interval: start period के दौरान जाँच कितनी बार चलती है। डिफ़ॉल्ट 5s है, और इसके लिए Docker Engine 25.0 या उससे नया संस्करण चाहिए।
महत्वपूर्ण नियम यह है: start period के दौरान एक विफल जाँच retries में नहीं गिनी जाती है, और कंटेनर starting में रहता है। पहली बार जब जाँच सफल होती है, तो कंटेनर healthy हो जाता है और start period तुरंत समाप्त हो जाता है, भले ही इसका अधिकांश समय उपयोग न हुआ हो। यदि जाँच विफल रहते हुए ही start period समाप्त हो जाता है, तो सामान्य काउंटडाउन शुरू हो जाता है, और कंटेनर को unhealthy चिह्नित होने से पहले लगातार retries विफलताओं की आवश्यकता होती है।
इसलिए कंटेनर के शुरू होने से unhealthy तक का सबसे खराब समय start_period प्लस retries गुणा interval प्लस timeout है। ऊपर दी गई फ़ाइल में मानों के साथ यह 30 प्लस 5 गुणा 13 है, जो 95 सेकंड है। deploy timeout सेट करने से पहले उस संख्या को लिख लें, क्योंकि 60 सेकंड के बाद हार मानने वाला रोलआउट इस कंटेनर को कभी भी अंतिम स्थिति तक पहुँचते हुए नहीं देखेगा।
यहाँ सामान्य गलती धीमी शुरुआत को कवर करने के लिए retries को बढ़ाना है। यह एक बार काम करता है और फिर हमेशा के लिए नुकसान पहुँचाता है: जिस सर्विस को बूट होने के लिए 8 रिट्राय की आवश्यकता थी, वह अब प्रोडक्शन में कुछ भी पता चलने से पहले 8 लगातार विफलताओं को सहन करती है। इसके बजाय start_period का उपयोग करें, क्योंकि यह केवल पहली सफलता से पहले लागू होता है।
depends_on अपने आप में किसी चीज़ की गारंटी क्यों नहीं देता है
depends_on का संक्षिप्त रूप अधिकांश भ्रम का स्रोत है।
api:
depends_on:
- dbइसका केवल एक अर्थ है: api कंटेनर से पहले db कंटेनर को शुरू करें। Compose कंटेनर के बनने और शुरू होने का इंतज़ार करता है। यह PostgreSQL के पहली बार इनिशियलाइज़ेशन पूरा करने का इंतज़ार नहीं करता है, और यह पोर्ट 5432 के कनेक्शन स्वीकार करने का इंतज़ार नहीं करता है। आपका ऐप लगभग एक सेकंड बाद शुरू होता है, एक ऐसे पोर्ट से कनेक्ट करने का प्रयास करता है जिस पर अभी कुछ भी लिसन नहीं कर रहा है, और बंद हो जाता है। लॉग में आपको Connection refused दिखाई देता है, या जब सर्वर चालू तो होता है लेकिन रिकवर हो रहा होता है, तब FATAL: the database system is starting up दिखाई देता है।
लंबा रूप वही है जो लोग वास्तव में चाहते हैं:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition के तीन मान होते हैं। service_started संक्षिप्त रूप के समान है। service_healthy आश्रित सर्विस को तब तक रोके रखता है जब तक कि डिपेंडेंसी 'healthy' रिपोर्ट न कर दे, जो केवल तभी सार्थक होता है जब वह डिपेंडेंसी अपने compose फ़ाइल या इमेज में healthcheck को परिभाषित करती है। service_completed_successfully एक 'one shot' कंटेनर, जैसे कि डेटाबेस माइग्रेशन, के स्टेटस 0 के साथ एग्जिट होने का इंतज़ार करता है।
condition के साथ दो अतिरिक्त फ़ील्ड होते हैं। restart: true Compose को निर्देश देता है कि डिपेंडेंसी सर्विस को अपडेट करने के बाद इस सर्विस को रीस्टार्ट करे। required: false एक अनुपस्थित डिपेंडेंसी को एरर से घटाकर चेतावनी (warning) में बदल देता है।
अब वह सीमा जो लोगों को परेशान करती है। इन शर्तों का मूल्यांकन तब किया जाता है जब स्टैक चालू होता है। ये केवल स्टार्ट ऑर्डरिंग हैं, न कि सुपरविज़न नियम। यदि डेटाबेस सुबह तीन बजे रीस्टार्ट होता है, तो कोई भी service_healthy का पुनर्मूल्यांकन नहीं करता है और इसे फिर से पूरा करने के लिए आपके ऐप को कोई रीस्टार्ट नहीं करता है। आपके एप्लिकेशन कोड को अभी भी अपने आप फिर से कनेक्ट करने की क्षमता रखनी होगी। docker compose up --no-deps api डिज़ाइन के अनुसार इस पूरी प्रक्रिया को छोड़ देता है, और सीधे docker start के साथ कंटेनर शुरू करना भी ऐसा ही करता है।
एक ऐसा चेक लिखें जो तत्परता की जांच करे, न कि केवल यह कि कोई प्रोसेस मौजूद है
pgrep nginx जैसा चेक केवल यह सिद्ध करता है कि प्रोसेस टेबल में कोई एंट्री मौजूद है। यह इस बारे में कुछ भी सिद्ध नहीं करता कि सर्विस किसी अनुरोध का उत्तर दे सकती है या नहीं। एक वेब एप्लिकेशन अपने डेटाबेस पूल के समाप्त हो जाने के काफी समय बाद भी अपने लिसनिंग सॉकेट को खुला रख सकता है, और प्रोसेस चेक पूरी खराबी के दौरान भी 'ग्रीन' (सफल) बना रहता है।
कंटेनर से वह कार्य करने के लिए कहें जिसके लिए वह बना है:
- HTTP सर्विस के लिए, एक वास्तविक एंडपॉइंट का अनुरोध करें।
curl -fsS,-fके कारण 400 या उससे ऊपर के किसी भी स्टेटस पर नॉन-जीरो (non-zero) एग्जिट कोड देता है, इसलिए खराब ऐप से मिलने वाला 500 एरर एक विफल चेक माना जाता है। - PostgreSQL के लिए,
pg_isreadyका उपयोग करें, जो सर्वर द्वारा कनेक्शन स्वीकार करने पर 0, अस्वीकार करने पर 1, बिल्कुल प्रतिक्रिया न देने पर 2, और गलत पैरामीटर पास करने पर 3 एग्जिट कोड देता है। - Redis के लिए,
redis-cli pingका उपयोग करें, जोPONGप्रिंट करता है और 0 एग्जिट कोड देता है। - MariaDB के लिए, आधिकारिक इमेज में एक
healthcheck.shस्क्रिप्ट होती है, औरhealthcheck.sh --connect --innodb_initializedवह तरीका है जिसे इसके मेंटेनर्स ने प्रलेखित (document) किया है।
pg_isready में एक ऐसी समस्या है जिसके बारे में जानना आवश्यक है। खाली डेटा डायरेक्टरी के साथ पहली बार शुरू होने पर, आधिकारिक postgres इमेज एक अस्थायी सर्वर के विरुद्ध अपना इनिशियलाइजेशन चलाती है जो केवल Unix सॉकेट पर लिसन करता है। बिना होस्ट आर्ग्युमेंट वाला pg_isready उस सॉकेट का उपयोग करता है, इसलिए यह "कनेक्शन स्वीकार कर रहा है" का उत्तर दे सकता है जबकि TCP पोर्ट 5432 अभी भी आपके एप्लिकेशन के लिए बंद है। चेक को स्पष्ट रूप से TCP पर पॉइंट करें और समस्या हल हो जाएगी, क्योंकि अस्थायी सर्वर वहां उत्तर नहीं देता है।
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दोहरे डॉलर के निशान कोई टाइपिंग त्रुटि नहीं हैं। Compose फाइल को पढ़ते समय $VAR को स्वयं एक्सपैंड करता है, जो आपके होस्ट एनवायरनमेंट से एक वैल्यू को चेक में डाल देगा। $$ इसे एस्केप करके एक सिंगल $ में बदल देता है, ताकि कंटेनर के अंदर की शेल इसे कंटेनर के अपने एनवायरनमेंट के विरुद्ध एक्सपैंड करे।
एक postgres और app स्टैक जो सही क्रम में शुरू होता है
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:इसे शुरू करें और स्थितियों (states) में बदलाव देखें:
docker compose up -d
docker compose psSTATUS कॉलम कोष्ठक में स्वास्थ्य स्थिति (health state) दर्शाता है। एक स्वस्थ पेयर (pair) में दोनों पंक्तियों पर Up 41 seconds (healthy) दिखाई देता है। जब डेटाबेस अभी भी इनिशियलाइज़ हो रहा होता है, तो db में Up 4 seconds (health: starting) दिखाई देता है और api सूची से गायब होता है, क्योंकि Compose ने अभी तक इसे बनाया नहीं है।
यह देखने के लिए कि कोई चेक पास या फेल क्यों हुआ, हेल्थ लॉग पढ़ें:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker पिछले कुछ परिणामों को सुरक्षित रखता है, जिनमें से प्रत्येक में एक स्टार्ट टाइम, एक एंड टाइम, एक ExitCode और कमांड का Output होता है। संग्रहीत आउटपुट को छोटा (truncate) कर दिया जाता है, इसलिए जो चेक एक बड़ा पेज बॉडी प्रिंट करता है, वह आपको एक बेकार लॉग एंट्री देगा। चेक को शांत (quiet) रखें।
जब कोई कंटेनर unhealthy हो जाता है तो Docker क्या करता है
कुछ नहीं। यह वह उत्तर है जो लोगों को सबसे अधिक आश्चर्यचकित करता है।
एक सिंगल होस्ट पर Docker Engine किसी unhealthy कंटेनर को रीस्टार्ट नहीं करता है। restart: unless-stopped पॉलिसी मुख्य प्रोसेस के बंद होने पर प्रतिक्रिया देती है, और एक unhealthy कंटेनर बंद नहीं हुआ होता है। यह एक सप्ताह तक unhealthy स्थिति में रह सकता है जबकि Compose इसे ऐसे ही छोड़ देता है। Swarm mode unhealthy टास्क को बदल देता है, लेकिन एक सर्वर पर साधारण Compose स्टैक ऐसा नहीं करता है।
इसके लिए दो उचित विकल्प हैं। प्रोसेस को तब बंद होने के लिए कहें जब उसे पता चले कि वह खराब हो गई है, ताकि रीस्टार्ट पॉलिसी के पास कार्रवाई करने के लिए कुछ हो। या बाहर से स्थिति पर नज़र रखें और उस पर अलर्ट प्राप्त करें। एक Uptime Kuma मॉनिटर को उसी एंडपॉइंट पर पॉइंट करना जिसे आपका हेल्थचेक कॉल करता है, इसका मतलब है कि एक टूटी हुई निर्भरता दोनों जगहों पर दिखाई देगी, और आपको इसके बारे में उपयोगकर्ता के बजाय मॉनिटर से पता चलेगा। यदि ट्रैफ़िक एक Traefik reverse proxy के माध्यम से ऐप तक पहुँचता है, तो याद रखें कि प्रॉक्सी का बैकएंड का अपना नज़रिया Docker हेल्थ स्टेट से अलग होता है, इसलिए एक दूसरे की जगह नहीं ले सकता।
ऐसे चेक को डीबग करना जो कभी भी healthy नहीं होता
स्वयं उसी कंटेनर के अंदर सटीक कमांड चलाएं और एग्जिट कोड देखें:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"exit=0 यहाँ, जबकि कंटेनर अभी भी unhealthy रिपोर्ट कर रहा है, इसका मतलब है कि आपका compose test आपके द्वारा अभी टाइप किए गए कमांड से अलग है, आमतौर पर इसलिए क्योंकि जहाँ शेल सिंटैक्स की आवश्यकता थी वहाँ CMD का उपयोग किया गया था।
दो गलतियाँ बाकी अधिकांश समस्याओं का कारण बनती हैं। पहली गलत पोर्ट है। हेल्थचेक कंटेनर के अंदर चलता है, इसलिए इसे कंटेनर पोर्ट का उपयोग करना चाहिए, न कि पब्लिश किए गए होस्ट पोर्ट का। ports: - "8080:3000" के साथ एप्लिकेशन 3000 पर लिसन करता है, और http://localhost:8080 के विरुद्ध चेक हमेशा विफल रहता है जबकि साइट ब्राउज़र में ठीक काम करती है। दूसरी गलती गलत होस्ट है। चेक के अंदर, localhost वही कंटेनर है, जो खुद को चेक करने के लिए सही है लेकिन पड़ोसी को चेक करने के लिए गलत है, जहाँ आपको सर्विस नाम की आवश्यकता होती है, उदाहरण के लिए db।
एक अंतिम स्थिति का उल्लेख करना आवश्यक है: हेल्थचेक पास हो जाता है जबकि उपयोगकर्ता त्रुटियाँ देखते हैं। ऐसा तब होता है जब एंडपॉइंट किसी वास्तविक चीज़ को छुए बिना एक स्टैटिक 200 रिस्पॉन्स देता है। एक रेडीनेस एंडपॉइंट जो कभी डेटाबेस को क्वेरी नहीं करता, वह आपको यह नहीं बता सकता कि डेटाबेस डाउन है। इसे एक सस्ती वास्तविक क्वेरी चलाने के लिए कॉन्फ़िगर करें।
FAQ
मेरा एप्लिकेशन तब भी कनेक्ट क्यों नहीं हो पाता है जब depends_on यह बताता है कि डेटाबेस स्वस्थ है?
क्योंकि condition: service_healthy का मूल्यांकन केवल एक बार किया जाता है, जब स्टैक शुरू होता है। यह बाद में किसी भी चीज़ की निगरानी नहीं करता है। यदि डेटाबेस कंटेनर बाद में रीस्टार्ट होता है, तो Compose स्थिति को फिर से पूरा करने के लिए आपके एप्लिकेशन को रीस्टार्ट नहीं करता है, इसलिए आपके एप्लिकेशन कोड में अपना स्वयं का रीकनेक्ट और रिट्राई लॉजिक होना चाहिए। जब आप docker start या docker compose up --no-deps के साथ एक सिंगल कंटेनर शुरू करते हैं, तो यह स्थिति कुछ भी नहीं करती है।
क्या मुझे हेल्थचेक की आवश्यकता है यदि इमेज में पहले से ही एक परिभाषित है?
आमतौर पर नहीं, और इसे ओवरराइड करना अक्सर एक गलत कदम होता है, क्योंकि इमेज मेंटेन करने वाले को पता होता है कि उस सॉफ़्टवेयर के लिए रेडीनेस का क्या अर्थ है। अपना स्वयं का हेल्थचेक केवल तब जोड़ें जब इमेज चेक आपके सेटअप के लिए गलत हो, उदाहरण के लिए जब यह उस पोर्ट की जांच करता है जिसे आपने बदल दिया है। इमेज हेल्थचेक को बंद करने के लिए, सर्विस पर test: ["NONE"] या disable: true सेट करें।
क्या हेल्थचेक में curl या wget का उपयोग करना चाहिए?
उसका उपयोग करें जो इमेज में पहले से मौजूद है, और उस पर निर्भर होने से पहले docker compose exec <service> curl --version के साथ इसकी पुष्टि करें। कई Debian आधारित इमेज में इनमें से कोई भी नहीं होता है। Alpine आधारित इमेज में BusyBox wget होता है। केवल हेल्थचेक चलाने के लिए इमेज में कोई पैकेज न जोड़ें जब सॉफ़्टवेयर अपना स्वयं का क्लाइंट प्रदान करता हो, जैसे कि pg_isready या redis-cli।
क्या अस्वस्थ कंटेनर अपने आप रीस्टार्ट हो जाता है?
सिंगल होस्ट पर Docker Engine द्वारा ऐसा नहीं होता है। रीस्टार्ट नीतियां प्रोसेस के समाप्त होने पर प्रतिक्रिया करती हैं, न कि हेल्थ स्टेट पर, इसलिए एक अस्वस्थ कंटेनर तब तक चालू और खराब रहता है जब तक कि कोई अन्य चीज़ उस पर कार्रवाई न करे। या तो विफलता का पता चलने पर प्रोसेस को समाप्त कर दें, या एक बाहरी मॉनिटर चलाएं जो स्थिति पर अलर्ट दे।
start_period कितना लंबा होना चाहिए?
यह आपके द्वारा मापे गए सबसे धीमे वैध पहले स्टार्ट के लिए पर्याप्त लंबा होना चाहिए, साथ ही थोड़ा मार्जिन भी रखें। इसे खाली वॉल्यूम के विरुद्ध docker compose up के साथ मापें, क्योंकि डेटाबेस का पहला स्टार्ट उसके बाद के हर स्टार्ट की तुलना में बहुत धीमा होता है। बहुत लंबी स्टार्ट अवधि केवल पहले unhealthy निर्णय में देरी करती है। बहुत अधिक रिट्राई कंटेनर के पूरे जीवनकाल के लिए चेक को कमजोर कर देते हैं, जो कि एक बड़ी विफलता है।