Docker Compose healthcheck सही तरीके से कैसे सेट करें
Docker Compose में healthcheck कैसे काम करते हैं और depends_on का उपयोग क्यों पर्याप्त नहीं है। Postgres और अपने ऐप के लिए सही readiness check लिखने का तरीका विस्तार से जानें।
Docker Compose healthcheck वास्तव में क्या करता है
Docker Compose healthcheck एक ऐसा कमांड है जिसे Docker एक टाइमर पर कंटेनर के अंदर चलाता है। Docker आपके लॉग्स नहीं पढ़ता, आपके पोर्ट पर नज़र नहीं रखता, और न ही आपकी प्रोसेस लिस्ट की जाँच करता है। यह केवल कमांड चलाता है, उसका exit code पढ़ता है, और कंटेनर पर एक एकल स्थिति (state) स्टोर करता है: starting, healthy, या unhealthy। Exit code 0 का अर्थ है healthy। कोई भी अन्य exit code unhealthy दर्शाता है, और exit code 2 को Docker द्वारा आरक्षित रखा गया है, इसलिए इसे जानबूझकर कभी न लौटाएं।
यही पूरी कार्यप्रणाली है। लगभग हर healthcheck समस्या एक ही होती है: आपके द्वारा लिखा गया कमांड उस प्रश्न का उत्तर देता है जो आप पूछना नहीं चाहते थे। यह गाइड मानती है कि आप पहले से ही जानते हैं कि VPS पर compose file कैसे लिखें, और यह वहां से शुरू होती है जहाँ स्टैक गलत क्रम में शुरू होता है।
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 कैसे काम करते हैं
timing को नियंत्रित करने के लिए पाँच settings होती हैं। इनके defaults Docker Engine से आते हैं, Compose से नहीं।
interval: container के start period के बाद दो checks के बीच का समय। Default 30s है।timeout: एक check को पूरा होने के लिए मिलने वाला समय, जिसके बाद Docker उसे kill कर देता है और उसे failure मानता है। Default 30s है।retries: state केunhealthyमें बदलने से पहले आवश्यक लगातार failures की संख्या। Default 3 है।start_period: container start होने के बाद का grace window। Default 0s है।start_interval: start period के दौरान check कितनी बार चलेगा। Default 5s है, और इसके लिए Docker Engine 25.0 या उससे नया version चाहिए।
महत्वपूर्ण नियम यह है: start period के दौरान, असफल check retries में नहीं गिना जाता है, और container starting में ही रहता है। जिस पहली बार check सफल होता है, container healthy हो जाता है और start period तुरंत समाप्त हो जाता है, भले ही उसका अधिकांश समय बचा हो। यदि check विफल रहते हुए ही start period समाप्त हो जाता है, तो सामान्य countdown शुरू हो जाता है, और container को unhealthy चिह्नित होने से पहले लगातार retries failures की आवश्यकता होती है।
अतः container start होने से लेकर unhealthy तक का सबसे खराब स्थिति वाला समय start_period जमा retries गुणा interval जमा timeout होता है। ऊपर दी गई file के मानों के साथ, यह 30 जमा 5 गुणा 13 है, जो कि 95 seconds है। deploy timeout set करने से पहले इस संख्या को नोट कर लें, क्योंकि 60 seconds के बाद हार मानने वाला rollout कभी भी इस container को final state तक नहीं पहुँचने देगा।
यहाँ सबसे आम गलती slow start को कवर करने के लिए retries को बढ़ाना है। यह एक बार तो काम करता है, लेकिन हमेशा के लिए नुकसानदेह हो जाता है: जिस service को boot होने के लिए 8 retries की आवश्यकता थी, वह अब production में 8 लगातार failures को तब तक सहन करेगी जब तक कि कोई उसे notice न करे। इसके बजाय 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' रिपोर्ट न कर दे, जो केवल तभी सार्थक होता है जब वह डिपेंडेंसी healthcheck को परिभाषित करती है, चाहे वह compose फाइल में हो या उसके इमेज में। service_completed_successfully एक 'one-shot' कंटेनर, जैसे कि डेटाबेस माइग्रेशन, के स्टेटस 0 के साथ एग्जिट होने का इंतजार करता है।
condition के साथ दो अतिरिक्त फील्ड होते हैं। restart: true Compose को यह निर्देश देता है कि डिपेंडेंसी सर्विस अपडेट होने के बाद इस सर्विस को रीस्टार्ट करे। required: false एक अनुपस्थित डिपेंडेंसी को एरर से घटाकर केवल एक चेतावनी (warning) में बदल देता है।
अब वह सीमा जो लोगों को परेशान करती है। इन शर्तों का मूल्यांकन तब किया जाता है जब स्टैक ऊपर आता है। ये केवल स्टार्ट होने का क्रम हैं, न कि कोई सुपरविज़न नियम। यदि डेटाबेस रात के तीन बजे रीस्टार्ट होता है, तो कोई भी service_healthy का पुनर्मूल्यांकन नहीं करता है और इसे फिर से पूरा करने के लिए आपके ऐप को कोई रीस्टार्ट नहीं करता है। आपके एप्लिकेशन कोड को अभी भी अपने आप रीकनेक्ट करने की क्षमता रखनी होगी। docker compose up --no-deps api डिज़ाइन के अनुसार इस पूरी प्रक्रिया को छोड़ देता है, और सीधे docker start के साथ कंटेनर शुरू करना भी ऐसा ही करता है।
एक ऐसा चेक लिखें जो readiness की जांच करे, न कि केवल यह कि process मौजूद है
pgrep nginx जैसा चेक केवल यह सिद्ध करता है कि process table में कोई entry मौजूद है। यह इस बारे में कुछ भी सिद्ध नहीं करता कि service किसी request का उत्तर दे सकती है या नहीं। एक web application अपने database pool के समाप्त होने के काफी देर बाद तक भी अपना listening socket खुला रख सकती है, और process check पूरी outage के दौरान green बना रहता है।
Container से वह काम करने के लिए कहें जिसके लिए वह बना है:
- HTTP service के लिए, एक वास्तविक endpoint का अनुरोध करें।
curl -fsS,-fके कारण 400 या उससे ऊपर की किसी भी status पर non-zero exit देता है, इसलिए एक खराब app से आने वाला 500 status एक failed check है। - PostgreSQL के लिए,
pg_isreadyका उपयोग करें, जो server द्वारा connections स्वीकार करने पर 0, अस्वीकार करने पर 1, बिल्कुल प्रतिक्रिया न देने पर 2, और आपके द्वारा दिए गए parameters गलत होने पर 3 exit देता है। - Redis के लिए,
redis-cli pingका उपयोग करें, जोPONGप्रिंट करता है और 0 exit देता है। - MariaDB के लिए, official image में एक
healthcheck.shscript होती है, औरhealthcheck.sh --connect --innodb_initializedवह तरीका है जिसे इसके maintainers document करते हैं।
pg_isready में एक ऐसी खामी है जिसके बारे में जानना उपयोगी है। खाली data directory के साथ अपनी पहली शुरुआत पर, official postgres image एक temporary server के विरुद्ध अपना initialisation चलाती है जो केवल Unix socket पर listen करता है। बिना host argument वाला pg_isready उस socket का उपयोग करता है, इसलिए यह "accepting connections" का उत्तर दे सकता है जबकि TCP port 5432 अभी भी आपके application के लिए बंद हो। चेक को स्पष्ट रूप से TCP पर point करें और समस्या हल हो जाएगी, क्योंकि temporary server वहां उत्तर नहीं देता है।
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दोहरे dollar signs कोई typo नहीं हैं। Compose फाइल को पढ़ते समय $VAR को स्वयं expand करता है, जो आपके host environment से एक value को चेक में डाल देगा। $$ इसे एक single $ में escape कर देता है, ताकि container के अंदर की shell इसे container के अपने environment के विरुद्ध expand करे।
Postgres और app stack जो सही क्रम में start होते हैं
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:इसे start करें और states में होने वाले बदलावों को देखें:
docker compose up -d
docker compose psSTATUS कॉलम ब्रैकेट में health state दिखाता है। एक healthy pair के लिए दोनों rows में Up 41 seconds (healthy) दिखाई देना चाहिए। जब database initialising अवस्था में होता है, तब db में Up 4 seconds (health: starting) दिखता है और api सूची से गायब होता है, क्योंकि Compose ने अभी तक इसे create नहीं किया है।
यह देखने के लिए कि कोई check पास हुआ या fail, health log पढ़ें:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker पिछले कुछ परिणामों को सुरक्षित रखता है, जिनमें से प्रत्येक का start time, end time, ExitCode और command का Output होता है। संग्रहीत output truncated होता है, इसलिए यदि कोई check एक बड़ा page body print करता है, तो आपको एक बेकार log entry मिलेगी। Checks को quiet रखें।
जब container unhealthy हो जाता है तो Docker क्या करता है
कुछ नहीं। यह वह उत्तर है जो लोगों को सबसे अधिक आश्चर्यचकित करता है।
एक single host पर Docker Engine किसी unhealthy container को restart नहीं करता है। restart: unless-stopped policy मुख्य process के exit होने पर प्रतिक्रिया देती है, और एक unhealthy container exit नहीं हुआ होता है। यह एक सप्ताह तक unhealthy स्थिति में रह सकता है और Compose इसे ऐसे ही छोड़ देता है। Swarm mode unhealthy tasks को बदल देता है, लेकिन एक server पर साधारण Compose stack ऐसा नहीं करता है।
इसके लिए दो स्पष्ट विकल्प हैं। process को तब exit करने के लिए कहें जब उसे पता हो कि वह खराब हो गई है, ताकि restart policy के पास कार्य करने के लिए कुछ हो। या बाहर से स्थिति पर नजर रखें और उस पर alert प्राप्त करें। Uptime Kuma monitor को उसी endpoint पर point करना जिसे आपका healthcheck call करता है, इसका मतलब है कि एक broken dependency दोनों जगहों पर दिखाई देगी, और आपको इसके बारे में उपयोगकर्ता के बजाय monitor से पता चलेगा। यदि traffic Traefik reverse proxy के माध्यम से app तक पहुँचता है, तो याद रखें कि proxy का backend के प्रति अपना दृष्टिकोण Docker health state से अलग होता है, इसलिए एक दूसरे की जगह नहीं ले सकता।
ऐसी चेक को डीबग करना जो कभी healthy नहीं होती
उसी container के भीतर स्वयं सटीक command चलाएँ और exit code देखें:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"exit=0 यहाँ तब आता है जब container अभी भी unhealthy रिपोर्ट कर रहा हो, इसका मतलब है कि आपका compose test आपके द्वारा अभी टाइप की गई command से अलग है, आमतौर पर ऐसा इसलिए होता है क्योंकि जहाँ shell syntax की आवश्यकता थी वहाँ CMD का उपयोग किया गया था।
दो गलतियाँ बाकी अधिकांश समस्याओं का कारण बनती हैं। पहली गलत port है। Healthcheck container के अंदर चलती है, इसलिए इसे container port का उपयोग करना चाहिए, कभी भी published host port का नहीं। ports: - "8080:3000" के साथ application 3000 पर listen करती है, और http://localhost:8080 के विरुद्ध की गई चेक हमेशा विफल रहेगी जबकि साइट browser में ठीक काम कर रही होगी। दूसरी गलती गलत host है। चेक के अंदर, localhost वही container है, जो खुद की जाँच करने के लिए सही है लेकिन पड़ोसी की जाँच करने के लिए गलत है, जहाँ आपको service name की आवश्यकता होती है, उदाहरण के लिए db।
एक अंतिम स्थिति का उल्लेख करना आवश्यक है: healthcheck पास हो जाती है जबकि users को error दिखाई देती है। ऐसा तब होता है जब endpoint किसी वास्तविक चीज़ को छुए बिना static 200 return करता है। एक readiness endpoint जो कभी database query नहीं करता, वह आपको यह नहीं बता सकता कि database बंद हो चुका है। इसे एक सस्ती वास्तविक query चलाने के लिए कॉन्फ़िगर करें।
FAQ
मेरा app तब भी connect क्यों नहीं हो पाता जब depends_on कहता है कि database healthy है?
क्योंकि condition: service_healthy का मूल्यांकन केवल एक बार होता है, जब stack शुरू होता है। यह बाद में किसी भी चीज़ की निगरानी नहीं करता है। यदि database container बाद में restart होता है, तो Compose स्थिति को फिर से पूरा करने के लिए आपके application को restart नहीं करता है, इसलिए आपके application code में अपना स्वयं का reconnect और retry logic होना चाहिए। जब आप docker start या docker compose up --no-deps के साथ एक single container शुरू करते हैं, तो यह condition कुछ भी नहीं करती है।
क्या मुझे healthcheck की आवश्यकता है यदि image में पहले से ही एक परिभाषित है?
आमतौर पर नहीं, और इसे override करना अक्सर एक गलत कदम होता है, क्योंकि image maintainer जानता है कि उस software के लिए readiness का क्या अर्थ है। अपना स्वयं का check केवल तब जोड़ें जब image check आपके setup के लिए गलत हो, उदाहरण के लिए जब यह उस port को probe करता है जिसे आपने बदल दिया है। image healthcheck को बंद करने के लिए, service पर test: ["NONE"] या disable: true सेट करें।
क्या healthcheck को curl या wget का उपयोग करना चाहिए?
जो भी image में पहले से मौजूद हो उसका उपयोग करें, और उस पर भरोसा करने से पहले docker compose exec <service> curl --version के साथ इसकी पुष्टि करें। कई Debian आधारित images में इनमें से कोई भी नहीं होता है। Alpine आधारित images में BusyBox wget होता है। केवल healthcheck चलाने के लिए image में कोई package न जोड़ें जब software अपना स्वयं का client प्रदान करता हो, जैसे कि pg_isready या redis-cli।
क्या unhealthy container स्वचालित रूप से restart हो जाता है?
एक single host पर Docker Engine द्वारा ऐसा नहीं होता है। Restart policies process के exit होने पर प्रतिक्रिया देती हैं, न कि health state पर, इसलिए एक unhealthy container तब तक चालू और खराब रहता है जब तक कि कोई अन्य चीज़ उस पर कार्य न करे। या तो process को तब exit कराएं जब वह विफलता का पता लगाए, या एक external monitor चलाएं जो स्थिति पर alert दे।
start_period कितना लंबा होना चाहिए?
आपके द्वारा मापे गए सबसे धीमे वैध पहले start के लिए पर्याप्त लंबा, साथ ही कुछ अतिरिक्त समय। इसे एक खाली volume के विरुद्ध docker compose up के साथ मापें, क्योंकि database का पहला start उसके बाद के हर start की तुलना में बहुत धीमा होता है। बहुत लंबा start period केवल पहले unhealthy verdict में देरी करता है। बहुत अधिक retries container के पूरे जीवनकाल के लिए check को कमजोर कर देते हैं, जो कि एक बड़ी विफलता है।