Docker Compose healthcheck: PostgreSQL اور app readiness
جانیں Docker Compose healthcheck کیسے evaluate کرتا ہے، depends_on کیوں کافی نہیں، اور PostgreSQL و app کے لیے درست readiness checks اور exit codes کیسے لکھیں۔
Docker Compose healthcheck حقیقت میں کیا کرتا ہے
Docker Compose healthcheck ایک کمانڈ ہے جسے Docker مقررہ وقفے سے container کے اندر چلاتا ہے۔ Docker آپ کے logs نہیں پڑھتا، آپ کے port کی نگرانی نہیں کرتا، اور نہ ہی آپ کی process list کا معائنہ کرتا ہے۔ یہ کمانڈ چلاتا ہے، exit code پڑھتا ہے، اور container پر ایک ہی state محفوظ کرتا ہے: starting، healthy، یا unhealthy۔ Exit code 0 کا مطلب healthy ہے۔ کوئی بھی دوسرا exit code unhealthy کا مطلب ہے، اور exit code 2 کو Docker نے مخصوص کر رکھا ہے، اس لیے اسے جان بوجھ کر کبھی واپس نہ کریں۔
پورا طریقۂ کار یہی ہے۔ تقریباً ہر healthcheck مسئلہ دراصل ایک ہی مسئلہ ہوتا ہے: آپ کی لکھی ہوئی کمانڈ اس سوال کا جواب دیتی ہے جو آپ پوچھنا چاہتے تھے، نہ کہ اس سوال کا جو آپ کا مقصد تھا۔ یہ رہنما فرض کرتا ہے کہ آپ پہلے ہی VPS پر compose فائل لکھنے کا طریقہ جانتے ہیں، اور وہاں سے شروع ہوتا ہے جہاں stack غلط ترتیب میں شروع ہوتا ہے۔
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 value کی دو مفید صورتیں ہیں۔ CMD سے شروع ہونے والی list کمانڈ کو براہِ راست چلاتی ہے، shell کے بغیر، اس لیے pipes، && اور variable expansion کام نہیں کرتے۔ CMD-SHELL سے شروع ہونے والی list باقی حصے کو ایک string کے طور پر container کے اندر موجود /bin/sh -c کو بھیجتی ہے۔ جب check کے لیے shell syntax درکار ہو تو یہی طریقہ استعمال کریں۔ سادہ string کو CMD-SHELL سمجھا جاتا ہے۔ صرف ["NONE"] پر مشتمل list اس healthcheck کو ختم کر دیتی ہے جو image نے اپنے Dockerfile کے ذریعے شامل کیا تھا۔
Check container کے اندر چلتا ہے، اس لیے اس میں درج ہر binary اسی image میں موجود ہونی چاہیے۔ پہلے اس کی تصدیق کریں، کیونکہ curl کے بغیر slim image ایسا container بناتی ہے جو مستقل unhealthy رہتا ہے، جبکہ اس کی وجہ application log میں کبھی ظاہر نہیں ہوتی۔ اسے دستی طور پر آزمائیں:
docker compose exec api curl --versionغائب binary کا جواب OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown ہوتا ہے۔ Alpine پر مبنی images عموماً اس کے بجائے BusyBox wget فراہم کرتی ہیں، اس لیے check یوں ہو جاتا ہے: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"]۔
interval، timeout اور retries کو start_period کے ساتھ کیسے ملایا جاتا ہے
پانچ ترتیبات وقت کا تعین کرتی ہیں۔ ان کی طے شدہ قدریں Compose سے نہیں بلکہ Docker Engine سے آتی ہیں۔
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 سیکنڈ بعد رک جانے والا rollout اس کنٹینر کو کبھی حتمی حالت تک پہنچا ہوا نہیں دیکھ سکے گا۔
یہاں عام غلطی سست آغاز کو برداشت کرنے کے لیے retries بڑھانا ہے۔ یہ ایک بار کام کرتا ہے، لیکن پھر ہمیشہ نقصان پہنچاتا ہے: جو سروس شروع ہونے کے لیے 8 retries کی محتاج تھی، وہ اب production میں کسی کے نوٹس لینے سے پہلے 8 مسلسل ناکامیوں کو برداشت کرے گی۔ اس کے بجائے start_period استعمال کریں، کیونکہ یہ صرف پہلی کامیابی سے پہلے لاگو ہوتا ہے۔
صرف depends_on اپنے طور پر کوئی ضمانت نہیں دیتا
depends_on کی مختصر شکل زیادہ تر الجھن کا سبب بنتی ہے۔
api:
depends_on:
- dbاس کا مطلب صرف یہ ہے: api container سے پہلے db container شروع کریں۔ Compose، container کے بننے اور شروع ہونے تک انتظار کرتا ہے۔ یہ PostgreSQL کی پہلی بار ہونے والی initialization کے مکمل ہونے کا انتظار نہیں کرتا، اور نہ ہی port 5432 پر connection قبول ہونے کا انتظار کرتا ہے۔ آپ کی app تقریباً ایک سیکنڈ بعد شروع ہوتی ہے، ایسے port سے connect کرنے کی کوشش کرتی ہے جہاں ابھی کچھ listening نہیں کر رہا ہوتا، اور بند ہو جاتی ہے۔ log میں آپ کو Connection refused نظر آتا ہے، یا جب server چل رہا ہو لیکن ابھی recovery کر رہا ہو تو FATAL: the database system is starting up نظر آتا ہے۔
طویل شکل دراصل وہی ہے جو لوگ چاہتے ہیں:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition کی تین values ہیں۔ service_started مختصر شکل جیسی ہی ہے۔ service_healthy dependent service کو اس وقت تک روکے رکھتا ہے جب تک dependency خود کو healthy رپورٹ نہ کرے۔ یہ اسی وقت بامعنی ہوتا ہے جب dependency میں healthcheck تعریف کیا گیا ہو، چاہے وہ compose file میں ہو یا اس کی image میں۔ service_completed_successfully one shot container، مثلاً database migration، کے status 0 کے ساتھ exit ہونے کا انتظار کرتا ہے۔
condition کے ساتھ دو اضافی fields موجود ہیں۔ restart: true Compose کو بتاتا ہے کہ dependency service اپ ڈیٹ ہونے کے بعد اس service کو restart کرنا ہے۔ required: false missing dependency کو error کے بجائے warning میں تبدیل کر دیتا ہے۔
اب وہ حد جس سے لوگ اکثر متاثر ہوتے ہیں۔ ان conditions کا جائزہ stack کے شروع ہوتے وقت لیا جاتا ہے۔ یہ start ordering ہے، supervision rule نہیں۔ اگر database رات کے تین بجے restart ہو جائے تو service_healthy کا دوبارہ جائزہ نہیں لیا جاتا، اور نہ ہی اسے دوبارہ پورا کرنے کے لیے آپ کی app restart ہوتی ہے۔ آپ کے application code کو خود دوبارہ connect کرنا ہوگا۔ docker compose up --no-deps api جان بوجھ کر پورے mechanism کو نظرانداز کرتا ہے، اور container کو براہ راست docker start کے ذریعے شروع کرنا بھی یہی کرتا ہے۔
ایسا check لکھیں جو readiness کو جانچے، صرف process کے موجود ہونے کو نہیں
pgrep nginx جیسا check صرف یہ ثابت کرتا ہے کہ process table میں ایک entry موجود ہے۔ اس سے یہ ثابت نہیں ہوتا کہ service کسی request کا جواب دے سکتی ہے۔ کوئی web application اپنی listening socket اس وقت تک کھلی رکھ سکتی ہے جب تک اس کا database pool بند ہو چکا ہو، اور اس پوری خرابی کے دوران process check کامیاب رہ سکتا ہے۔
Container سے وہ کام کروائیں جس کے لیے اسے چلایا گیا ہے:
- HTTP service کے لیے حقیقی endpoint کو request کریں۔
curl -fsS،-fکی وجہ سے، 400 یا اس سے زیادہ کے کسی بھی status پر non zero کے ساتھ exit ہوتا ہے؛ اس لیے خراب application سے ملنے والا 500 failed check شمار ہوگا۔ - PostgreSQL کے لیے
pg_isreadyاستعمال کریں۔ جب server connections قبول کر رہا ہو تو یہ 0، connections مسترد کر رہا ہو تو 1، بالکل جواب نہ دے رہا ہو تو 2، اور فراہم کردہ parameters غلط ہوں تو 3 کے ساتھ exit ہوتا ہے۔ - Redis کے لیے
redis-cli pingاستعمال کریں۔ یہPONGprint کرتا ہے اور 0 کے ساتھ exit ہوتا ہے۔ - MariaDB کے لیے official image میں
healthcheck.shscript شامل ہے، اورhealthcheck.sh --connect --innodb_initializedوہ form ہے جسے اس کے maintainers document کرتے ہیں۔
pg_isready میں ایک اہم مسئلہ ہے۔ خالی data directory کے ساتھ پہلی مرتبہ start ہونے پر official postgres image اپنی initialisation ایک temporary server کے خلاف چلاتی ہے، جو صرف Unix socket پر listening کرتا ہے۔ بغیر host argument کے pg_isready اسی socket کو استعمال کرتا ہے۔ اس لیے یہ "accepting connections" کا جواب دے سکتا ہے، جبکہ application کے لیے TCP port 5432 ابھی بند ہو۔ Check کو واضح طور پر TCP پر بھیجیں؛ مسئلہ ختم ہو جائے گا، کیونکہ 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 نہیں ہیں۔ File پڑھتے وقت Compose خود $VAR کو expand کرتا ہے، جس سے آپ کے host environment کی value check میں شامل ہو سکتی ہے۔ $$ اسے ایک واحد $ میں escape کرتا ہے، تاکہ container کے اندر shell اسے container کے اپنے environment کے مطابق expand کرے۔
postgres اور app stack جو درست ترتیب سے شروع ہوتا ہے
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:اسے شروع کریں اور ریاستوں میں ہونے والی تبدیلی دیکھیں:
docker compose up -d
docker compose psSTATUS کالم قوسین میں health state دکھاتا ہے۔ صحت مند جوڑے کی دونوں قطاروں میں Up 41 seconds (healthy) ظاہر ہوتا ہے۔ جب database ابھی initialize ہو رہا ہو، تو db میں Up 4 seconds (health: starting) ظاہر ہوتا ہے، اور api فہرست میں موجود نہیں ہوتا، کیونکہ Compose نے اسے ابھی create نہیں کیا۔
یہ دیکھنے کے لیے کہ check کامیاب یا ناکام کیوں ہوا، health log پڑھیں:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker آخری چند نتائج محفوظ رکھتا ہے۔ ہر نتیجے میں start time، end time، ایک ExitCode، اور command کا Output شامل ہوتا ہے۔ محفوظ کیا گیا output مختصر کر دیا جاتا ہے، اس لیے جو check بڑی page body دکھاتا ہے، اس کی log entry بےکار ہو جاتی ہے۔ Checks کو خاموش رکھیں۔
جب container unhealthy ہو جائے تو Docker کیا کرتا ہے
کچھ نہیں۔ یہی وہ جواب ہے جو زیادہ تر لوگوں کو حیران کرتا ہے۔
ایک single host پر Docker Engine، unhealthy container کو دوبارہ شروع نہیں کرتا۔ restart: unless-stopped policy اس وقت ردِعمل دیتی ہے جب main process بند ہو جائے، جبکہ unhealthy container بند نہیں ہوا ہوتا۔ یہ ایک ہفتے تک unhealthy حالت میں رہ سکتا ہے اور Compose اسے نظرانداز کرتا رہتا ہے۔ Swarm mode unhealthy tasks کو تبدیل کرتا ہے، لیکن ایک server پر چلنے والا سادہ Compose stack ایسا نہیں کرتا۔
اس صورت میں دو واضح راستے ہیں۔ جب process کو معلوم ہو کہ وہ ناکام ہو چکا ہے تو اسے exit کروا دیں، تاکہ restart policy کے پاس کارروائی کرنے کی وجہ ہو۔ یا باہر سے state کو monitor کریں اور اس پر alert جاری کریں۔ Uptime Kuma monitor کو اسی endpoint پر لگانے سے، جسے آپ کا healthcheck استعمال کرتا ہے، خراب dependency دونوں جگہ ظاہر ہوتی ہے، اور آپ کو صارف کے بجائے monitor سے اطلاع ملتی ہے۔ اگر network traffic app تک Traefik reverse proxy کے ذریعے پہنچتا ہے، تو یاد رکھیں کہ backend کے بارے میں proxy کا اپنا نقطۂ نظر Docker health state سے الگ ہوتا ہے؛ اس لیے ایک دوسرے کی جگہ کافی نہیں ہوتا۔
ایسے check کی خرابی دور کرنا جو کبھی healthy نہیں ہوتا
درست command خود اسی container میں چلائیں اور exit code دیکھیں:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"اگر container ابھی بھی unhealthy رپورٹ کر رہا ہو اور یہاں exit=0 آ رہا ہو تو اس کا مطلب ہے کہ آپ کی compose test آپ کے ابھی درج کیے گئے متن سے مختلف ہے۔ عموماً ایسا اس لیے ہوتا ہے کہ shell syntax کی ضرورت کے باوجود CMD استعمال کیا گیا ہو۔
باقی زیادہ تر مسائل دو غلطیوں کی وجہ سے ہوتے ہیں۔ پہلی غلطی غلط port ہے۔ healthcheck container کے اندر چلتا ہے، اس لیے اسے container port استعمال کرنا چاہیے، شائع شدہ host port نہیں۔ ports: - "8080:3000" میں application، 3000 پر listen کرتی ہے۔ اگر check http://localhost:8080 کے خلاف ہو تو یہ ہمیشہ ناکام رہے گا، حالانکہ browser میں site درست کام کر رہی ہوگی۔ دوسری غلطی غلط host ہے۔ check کے اندر localhost اسی container کو ظاہر کرتا ہے۔ اپنے container کو check کرنے کے لیے یہ درست ہے، لیکن دوسرے container کو check کرنے کے لیے غلط ہے۔ دوسرے container کے لیے service name استعمال کریں، مثلاً db۔
ایک آخری صورتِ حال کا الگ ذکر ضروری ہے: healthcheck کامیاب ہو جاتا ہے، لیکن صارفین کو errors نظر آتی ہیں۔ ایسا اس وقت ہوتا ہے جب endpoint کسی حقیقی dependency کو استعمال کیے بغیر static 200 واپس کرتا ہے۔ ایسا 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 کے ساتھ صرف ایک container شروع کرتے ہیں تو یہ شرط بھی کوئی اثر نہیں کرتی۔
اگر image پہلے ہی healthcheck متعین کرتی ہے تو کیا مجھے healthcheck کی ضرورت ہے؟
عام طور پر نہیں۔ اسے override کرنا اکثر الٹا قدم ہوتا ہے، کیونکہ image maintainer جانتا ہے کہ اس software کے لیے readiness کا کیا مطلب ہے۔ اپنی healthcheck صرف اس وقت شامل کریں جب image کی check آپ کے setup کے لیے غلط ہو، مثلاً جب وہ ایسے port کی جانچ کرتی ہو جسے آپ نے منتقل کر دیا ہے۔ 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 چلتا رہتا ہے اور خراب رہتا ہے، جب تک کوئی دوسرا component کارروائی نہ کرے۔ یا تو failure کا پتا چلنے پر process کو exit کرائیں، یا ایسا external monitor چلائیں جو اس state پر alert دے۔
start_period کتنا ہونا چاہیے؟
اتنا کہ آپ کے ماپے ہوئے سب سے سست جائز first start کے لیے کافی ہو، اور اس کے علاوہ کچھ اضافی وقت بھی ہو۔ خالی volume کے ساتھ docker compose up استعمال کرکے وقت کی پیمائش کریں، کیونکہ database کا first start اس کے بعد ہونے والے ہر start سے بہت زیادہ سست ہوتا ہے۔ بہت طویل start period صرف پہلے unhealthy verdict میں تاخیر کرتا ہے۔ بہت زیادہ retries پورے container lifetime کے دوران check کو کم مؤثر بناتی ہیں، اور یہی زیادہ سنگین failure ہے۔