رفتن به محتوا
LoopX

healthcheck و ترتیب اجرا

توی این درس یاد می‌گیری چرا «کانتینر در حال اجرا است» یعنی هنوز «سرویس آماده است» نیست، و چطور با HEALTHCHECK (بررسی سلامت) این فاصله را اندازه بگیری. وضعیت‌های starting، healthy و unhealthy را با docker inspect --format '{{.State.Health.Status}}' می‌خوانی، با depends_on همراه condition: service_healthy می‌گویی هر سرویس فقط وقتی شروع شود که وابستگی‌اش واقعاً آماده است، و با restart policy رفتار پس از خطا را تنظیم می‌کنی. در تمرین اصلی اپ را طوری می‌نویسی که منتظر آماده شدن دیتابیس بماند.

مسئله: «روشن» با «آماده» فرق دارد

Section titled “مسئله: «روشن» با «آماده» فرق دارد”

دیتابیس یک کانتینر است. داکر وقتی پروسه‌ی اصلی‌اش شروع شد می‌گوید Up؛ ولی دیتابیس ممکن است ده ثانیه دیگر مشغول ساختن فایل‌ها و بازیابی تراکنش‌ها باشد و هنوز به هیچ اتصالی جواب ندهد. اگر اپ در همان لحظه بالا بیاید و وصل شود، با «Connection refused» می‌میرد.

تشبیه: پرسیدن «نفس می‌کشی؟»

Section titled “تشبیه: پرسیدن «نفس می‌کشی؟»”

کانتینر مثل آدمی است که چشمش باز است (پروسه روشن است). HEALTHCHECK مثل این است که هر چند ثانیه یک پرستار بیاید و بپرسد «می‌توانی جواب بدهی؟». اگر چند بار پشت سر هم جواب نداد، برچسب «ناسالم» می‌خورد. دیگران (مثل اپ شما) می‌توانند منتظر برچسب «سالم» بمانند.

healthcheck مرتب اجرا می‌شود. تا وقتی هنوز در start_period است failure شمرده نمی‌شود. بعد از چند موفقیت healthy و بعد از retries شکست پشت‌سرهم unhealthy می‌شود، و با یک موفقیت دوباره healthy.

دستور سلامت (test) داخل خود کانتینر اجرا می‌شود. کد خروج ۰ یعنی سالم، ۱ یعنی ناسالم.

مثال ۱: healthcheck روی docker run

Section titled “مثال ۱: healthcheck روی docker run”
Terminal window
docker run -d --name lx-hc1 \
--health-cmd 'wget -qO /dev/null http://localhost/ || exit 1' \
--health-interval 2s --health-timeout 2s --health-retries 3 \
nginx:alpine >/dev/null
echo "فوراً: $(docker inspect --format '{{.State.Health.Status}}' lx-hc1)"
sleep 6
echo "بعد از ۶ث: $(docker inspect --format '{{.State.Health.Status}}' lx-hc1)"
docker ps --filter name=lx-hc1 --format '{{.Names}}: {{.Status}}'
خروجی
فوراً: starting
بعد از ۶ث: healthy
lx-hc1: Up 6 seconds (healthy)

بلافاصله starting است (هنوز اولین بررسی انجام نشده)، بعد از چند ثانیه healthy. در docker ps هم داخل ستون Status می‌نویسد (healthy). دستور سلامت wget -qO /dev/null http://localhost/ است: اگر nginx جواب بدهد exit 0، وگرنه exit 1.

⚡ بررسی سریع

کد خروج کدام یعنی «سالم»؟

مثال ۲: unhealthy شدن و بازیابی

Section titled “مثال ۲: unhealthy شدن و بازیابی”
Terminal window
docker run -d --name lx-hc2 \
--health-cmd 'test -f /ready' --health-interval 1s --health-retries 2 \
alpine sleep 300 >/dev/null
sleep 5
echo "وضعیت سلامت: $(docker inspect --format '{{.State.Health.Status}}' lx-hc2)"
echo "وضعیت کانتینر: $(docker inspect --format '{{.State.Status}}' lx-hc2)"
docker exec lx-hc2 touch /ready
sleep 3
echo "بعد از ساخت /ready: $(docker inspect --format '{{.State.Health.Status}}' lx-hc2)"
خروجی
وضعیت سلامت: unhealthy
وضعیت کانتینر: running
بعد از ساخت /ready: healthy

فایل /ready نبود، پس دو بار شکست خورد و unhealthy شد. اما کانتینر هنوز running است: داکر خودش کانتینر ناسالم را نمی‌کشد و ریستارت نمی‌کند (فقط برچسب می‌زند). وقتی فایل ساخته شد، با یک موفقیت دوباره healthy شد.

Terminal window
docker inspect --format '{{range .State.Health.Log}}exit={{.ExitCode}} {{end}}' lx-hc2 | cut -c1-80
docker inspect --format '{{json .Config.Healthcheck}}' lx-hc2
خروجی
exit=1 exit=1 exit=1 exit=0 exit=0
{"Test":["CMD-SHELL","test -f /ready"],"Interval":1000000000,"Retries":2}

آخرین چند نتیجه‌ی بررسی (کد خروج هر کدام) در State.Health.Log است؛ برای عیب‌یابی «چرا ناسالم است؟» همین را (همراه Output) ببین. و تنظیمات healthcheck در Config.Healthcheck (زمان‌ها به نانوثانیه: 1000000000 = ۱ ثانیه).

مثال ۴: HEALTHCHECK داخل Dockerfile

Section titled “مثال ۴: HEALTHCHECK داخل Dockerfile”
hcimg/Dockerfile
FROM alpine
HEALTHCHECK --interval=2s --timeout=2s --retries=2 --start-period=1s \
CMD test -f /ready || exit 1
CMD ["sleep", "300"]
Terminal window
docker build -q -t lx-hcimg hcimg >/dev/null
docker run -d --name lx-hc3 lx-hcimg >/dev/null
sleep 6
echo "اپ داخل image: $(docker inspect --format '{{.State.Health.Status}}' lx-hc3)"
docker run -d --name lx-hc4 --no-healthcheck lx-hcimg >/dev/null
sleep 1
echo "با --no-healthcheck: $(docker inspect --format '{{.State.Health}}' lx-hc4)"
خروجی
اپ داخل image: unhealthy
با --no-healthcheck: <nil>

وقتی HEALTHCHECK را در Dockerfile بنویسی، هر کانتینری از آن image با آن بررسی بالا می‌آید. می‌توانی موقع اجرا با --health-cmd عوضش کنی یا با --no-healthcheck خاموشش کنی (کانتینر چهارم هیچ وضعیت سلامتی ندارد: <nil>).

گزینه در HEALTHCHECK پیش‌فرض معنی
--interval ۳۰s فاصله‌ی بین بررسی‌ها
--timeout ۳۰s بیشترین زمان یک بررسی؛ بیشتر شد = شکست
--retries ۳ چند شکست پشت‌سرهم تا unhealthy
--start-period ۰s مهلت راه‌اندازی؛ شکست‌ها در آن شمرده نمی‌شوند

مثال ۵: بدون شرط، اپ زودتر از دیتابیس شروع می‌شود

Section titled “مثال ۵: بدون شرط، اپ زودتر از دیتابیس شروع می‌شود”

دیتابیس ما یک Redis است که عمداً ۶ ثانیه دیر بالا می‌آید (مثل یک دیتابیس سنگین). اپ ما فقط به آن ping می‌زند.

hcn/compose.yaml
name: lxhcn
services:
db:
image: redis:alpine
command: sh -c "sleep 6; exec redis-server"
app:
image: redis:alpine
depends_on:
- db
command: sh -c 'redis-cli -h db ping'

depends_on: [db] (شکل ساده) فقط می‌گوید «کانتینر db را زودتر شروع کن»، نه اینکه صبر کن آماده شود:

Terminal window
cd hcn
docker compose up -d >/dev/null 2>&1
sleep 2
docker compose logs app 2>&1 | sed -E 's/^[^|]+\| //'
docker compose down -v >/dev/null 2>&1
خروجی
Could not connect to Redis at db:6379: Connection refused

اپ رسید، دیتابیس هنوز جواب نمی‌داد (Connection refused) و اپ شکست خورد. همان مسابقه‌ی زمانی مسئله‌ی بالا.

hc/compose.yaml
name: lxhc
services:
db:
image: redis:alpine
command: sh -c "sleep 6; exec redis-server"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 2s
retries: 5
start_period: 3s
app:
image: redis:alpine
depends_on:
db:
condition: service_healthy
command: sh -c 'redis-cli -h db ping'

حالا دیتابیس healthcheck دارد و اپ منتظر healthy شدن آن می‌ماند:

Terminal window
cd hc
t0=$(date +%s)
docker compose up -d >/dev/null 2>&1
t1=$(date +%s)
[ $((t1 - t0)) -ge 5 ] && echo "up صبر کرد تا db سالم شود (بیش از ۵ ثانیه)"
docker inspect --format 'db: {{.State.Health.Status}}' lxhc-db-1
sleep 1
docker compose logs app 2>&1 | sed -E 's/^[^|]+\| //'
docker compose down -v >/dev/null 2>&1
خروجی
up صبر کرد تا db سالم شود (بیش از ۵ ثانیه)
db: healthy
PONG

up تا سالم شدن دیتابیس صبر کرد، بعد اپ شروع شد و PONG گرفت. بدون هیچ sleep و حدس‌زدن. بخش condition سه مقدار دارد:

condition یعنی
service_started (پیش‌فرض) کانتینر شروع شده باشد
service_healthy healthcheck آن سرویس healthy باشد
service_completed_successfully سرویس تمام شده و با کد ۰ خارج شده باشد (مثلاً migration)
⚡ بررسی سریع

depends_on: [db] (شکل ساده) چه تضمینی می‌دهد؟

مثال ۷: الگوی جایگزین، restart policy و تلاش دوباره

Section titled “مثال ۷: الگوی جایگزین، restart policy و تلاش دوباره”

بعضی اپ‌ها باید خودشان با خطای موقت کنار بیایند (چون دیتابیس در عمل هم ممکن است وسط کار برود). یک راه ساده: restart: on-failure تا وقتی اپ با خطا تمام می‌شود دوباره امتحانش کند.

hcr/compose.yaml
name: lxhcr
services:
db:
image: redis:alpine
command: sh -c "sleep 6; exec redis-server"
app:
image: redis:alpine
restart: on-failure:10
command: sh -c 'redis-cli -h db ping | grep -q PONG || exit 1; echo "اتصال برقرار شد"'
Terminal window
cd hcr
docker compose up -d >/dev/null 2>&1
for i in $(seq 40); do
s=$(docker inspect --format '{{.State.Status}}' lxhcr-app-1)
[ "$s" = exited ] && break
sleep 1
done
n=$(docker inspect --format '{{.RestartCount}}' lxhcr-app-1)
[ "$n" -ge 1 ] && echo "اپ چند بار دوباره تلاش کرد (RestartCount >= 1)"
docker inspect --format 'کد خروج نهایی: {{.State.ExitCode}}' lxhcr-app-1
docker compose logs app 2>&1 | sed -E 's/^[^|]+\| //' | tail -1
docker compose down -v >/dev/null 2>&1
خروجی
اپ چند بار دوباره تلاش کرد (RestartCount >= 1)
کد خروج نهایی: 0
اتصال برقرار شد

اپ چند بار شکست خورد و دوباره بالا آمد، تا دیتابیس آماده شد و در تلاش بعدی موفق (کد خروج ۰) شد؛ دیگر ریستارت نشد. on-failure:10 یعنی حداکثر ۱۰ بار تلاش. راه امن‌تر ترکیب هر دو است: service_healthy برای راه‌اندازی، و restart برای خرابی‌های وسط کار.

وقتی healthcheck تعریف شده، dockerd برای هر کانتینر یک تایمر نگه می‌دارد. هر interval یک بار دستور test را با همان کاری که docker exec می‌کند داخل کانتینر اجرا می‌کند و نتیجه (کد خروج و ۴ کیلوبایت آخر خروجی) را در State.Health.Log می‌گذارد (حداکثر ۵ ورودی آخر). شمارنده‌ی FailingStreak شکست‌های متوالی را می‌شمارد؛ وقتی به retries رسید وضعیت unhealthy می‌شود.

دو نکته‌ی مهم:

  • داکر خودش کانتینر unhealthy را ریستارت نمی‌کند (این کار Swarm و ابزارهای بالاتر است). restart policy فقط به خروج پروسه واکنش نشان می‌دهد نه به برچسب سلامت. اگر می‌خواهی ناسالم بمیرد، خود اسکریپت test یا اپ باید exit کند.
  • depends_on با service_healthy فقط در زمان docker compose up اعمال می‌شود. اگر بعداً دیتابیس بیفتد، Compose اپ را نمی‌بندد؛ اپ باید خودش دوباره‌اتصال (reconnect) بزند.

دستور سلامت را ساده و سریع نگه دار (مثلاً redis-cli ping، pg_isready، wget به یک مسیر /health)، چون هر interval اجرا می‌شود. بررسی سنگین کانتینر را کند می‌کند.

جدول مرجع: تنظیمات healthcheck در Compose

Section titled “جدول مرجع: تنظیمات healthcheck در Compose”
کلید معنی
test ["CMD", "cmd", "arg"] یا ["CMD-SHELL", "cmd با شل"] یا ["NONE"]
interval فاصله‌ی بررسی‌ها
timeout بیشترین زمان یک بررسی
retries شکست‌های پشت‌سرهم تا unhealthy
start_period مهلت راه‌اندازی
disable: true خاموش کردن healthcheck ارثی از image
restart معنی
no دوباره شروع نشود (پیش‌فرض)
on-failure[:N] فقط با کد خروج غیرصفر، حداکثر N بار
always همیشه (حتی بعد از ریستارت داکر)
unless-stopped همیشه، مگر دستی متوقف شده باشد

۱) فرض کردن اینکه depends_on ساده کافی است

Section titled “۱) فرض کردن اینکه depends_on ساده کافی است”

مثال ۵: اپ زودتر شروع شد و شکست خورد. راه‌حل: condition: service_healthy و healthcheck روی وابستگی.

۲) condition: service_healthy بدون healthcheck

Section titled “۲) condition: service_healthy بدون healthcheck”
Terminal window
mkdir -p m2 && cat > m2/compose.yaml <<'LXEOF'
services:
db:
image: alpine
command: sleep 30
app:
image: alpine
depends_on:
db:
condition: service_healthy
LXEOF
cd m2 && docker compose up -d 2>&1 | grep "no healthcheck"
docker compose down >/dev/null 2>&1
خروجی
dependency failed to start: container m2-db-1 has no healthcheck configured

سرویسی که healthcheck ندارد هرگز healthy نمی‌شود، پس Compose خطا می‌دهد. راه‌حل: برای db یک healthcheck تعریف کن.

۳) دستور سلامتی که در image نیست

Section titled “۳) دستور سلامتی که در image نیست”
Terminal window
docker run -d --name lx-hc5 --health-cmd 'pg_isready' --health-interval 1s --health-retries 1 nginx:alpine >/dev/null
sleep 4
docker inspect --format '{{.State.Health.Status}}: {{(index .State.Health.Log 0).Output}}' lx-hc5 | head -2
docker rm -f lx-hc5 >/dev/null
خروجی
unhealthy: /bin/sh: pg_isready: not found

nginx کاملاً سالم است، ولی ابزار pg_isready در این image وجود ندارد، پس بررسی همیشه شکست می‌خورد و سرویس سالم هم «ناسالم» می‌شود. همین اتفاق با curl در imageهای سبکی که curl ندارند می‌افتد. راه‌حل: ابزاری را بنویس که در همان image هست (مثل wget در busybox، redis-cli در Redis، pg_isready در Postgres)، و خروجی State.Health.Log را بخوان.

۴) start_period کم برای سرویس کند

Section titled “۴) start_period کم برای سرویس کند”

دیتابیسی که ۳۰ ثانیه برای راه‌اندازی لازم دارد، با retries: 3 و interval: 5s بعد از ۱۵ ثانیه unhealthy اعلام می‌شود، حتی اگر نیمه‌راه راه‌اندازی باشد. راه‌حل: start_period را به اندازه‌ی راه‌اندازی بگذار.

۵) انتظار اینکه unhealthy کانتینر را ریستارت کند

Section titled “۵) انتظار اینکه unhealthy کانتینر را ریستارت کند”

مثال ۲: کانتینر ناسالم هنوز running بود. راه‌حل: ریستارت را با خروج پروسه یا ابزار هماهنگی (Swarm، Kubernetes) انجام بده؛ در Compose خالص، فقط برچسب است و service_healthy.

✎ تمرینآسان

یک کانتینر redis:alpine با healthcheck (redis-cli ping) اجرا کن و نشان بده بعد از چند ثانیه healthy می‌شود.

دیدن جواب
Terminal window
docker run -d --name lx-e1 --health-cmd 'redis-cli ping' --health-interval 1s redis:alpine >/dev/null
sleep 4
docker inspect --format 'وضعیت: {{.State.Health.Status}}' lx-e1
docker rm -f lx-e1 >/dev/null
خروجی
وضعیت: healthy
✎ تمرینمتوسط

تمرین اصلی: اپ را طوری تنظیم کن که منتظر آماده شدن دیتابیس بماند. یک db (redis که ۵ ثانیه دیر بالا می‌آید و healthcheck دارد) و یک app با condition: service_healthy بنویس، با docker compose up -d --wait اجرا کن و با docker compose ps نشان بده db در حالت healthy است.

دیدن جواب
Terminal window
mkdir -p e2 && cat > e2/compose.yaml <<'LXEOF'
name: lxe2
services:
db:
image: redis:alpine
command: sh -c "sleep 5; exec redis-server"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
retries: 10
app:
image: redis:alpine
depends_on:
db:
condition: service_healthy
command: sh -c 'redis-cli -h db ping && sleep 300'
LXEOF
cd e2
docker compose up -d --wait 2>&1 | grep -E "Healthy|Started" | sed -E 's/^ +//' | sort -u
docker compose ps --format '{{.Service}}: {{.State}} {{.Health}}' | sort
docker compose down -v >/dev/null 2>&1
خروجی
Container lxe2-app-1 Healthy
Container lxe2-app-1 Started
Container lxe2-db-1 Healthy
Container lxe2-db-1 Started
app: running
db: running healthy

--wait دستور را تا آماده شدن همه‌ی سرویس‌ها نگه می‌دارد؛ برای اسکریپت‌ها و CI عالی است.

✎ تمرینسخت

یک image با HEALTHCHECK بساز که تا وقتی فایل /app/ready وجود ندارد ناسالم باشد. کانتینر را اجرا کن، نشان بده ناسالم است، فایل را بساز و نشان بده سالم شد؛ سپس همان image را با --no-healthcheck اجرا کن و نشان بده وضعیت سلامتی ندارد.

دیدن جواب
Terminal window
mkdir -p e3 && cat > e3/Dockerfile <<'LXEOF'
FROM alpine
RUN mkdir /app
HEALTHCHECK --interval=1s --retries=2 CMD test -f /app/ready
CMD ["sleep", "300"]
LXEOF
docker build -q -t lx-hce3 e3 >/dev/null
docker run -d --name lx-e3a lx-hce3 >/dev/null
sleep 4
echo "ابتدا: $(docker inspect --format '{{.State.Health.Status}}' lx-e3a)"
docker exec lx-e3a touch /app/ready; sleep 3
echo "بعد: $(docker inspect --format '{{.State.Health.Status}}' lx-e3a)"
docker run -d --name lx-e3b --no-healthcheck lx-hce3 >/dev/null
echo "بدون: $(docker inspect --format '{{.State.Health}}' lx-e3b)"
docker rm -f lx-e3a lx-e3b >/dev/null; docker rmi lx-hce3 >/dev/null
خروجی
ابتدا: unhealthy
بعد: healthy
بدون: <nil>
؟ آزمونک
  1. تفاوت «کانتینر در حال اجرا» و «سرویس آماده» چیست؟

  2. کد خروج دستور healthcheck برای «ناسالم» چیست؟

  3. depends_on: [db] چه می‌کند؟

  4. کانتینری unhealthy شد؛ داکر چه می‌کند؟

  5. start_period برای چیست؟

  6. condition: service_completed_successfully برای چیست؟

  • «در حال اجرا» ≠ «آماده». HEALTHCHECK (یا --health-cmd / healthcheck: در Compose) آماده بودن را می‌سنجد.
  • وضعیت‌ها: starting ← healthy / unhealthy؛ با docker inspect --format '{{.State.Health.Status}}' می‌خوانی؛ لاگ در State.Health.Log.
  • depends_on ساده فقط ترتیب شروع است؛ برای آماده بودن condition: service_healthy و برای کارهای تک‌بار service_completed_successfully.
  • docker compose up -d --wait تا آماده شدن سرویس‌ها صبر می‌کند.
  • داکر کانتینر ناسالم را خودش ریستارت نمی‌کند؛ restart فقط به خروج پروسه واکنش دارد. اپ باید reconnect هم بلد باشد.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker inspect --format '{{.State.Health.Status}}' Cوضعیت سلامت
docker run --health-cmd CMD --health-interval 5s IMGhealthcheck موقع run
docker run --no-healthcheck IMGخاموش کردن healthcheck image
HEALTHCHECK --interval=5s CMD cmd || exit 1در Dockerfile
healthcheck: {test: [CMD, redis-cli, ping]}در Compose
depends_on: {db: {condition: service_healthy}}منتظر سالم شدن db
docker compose up -d --waitصبر تا آماده شدن سرویس‌ها
restart: on-failure:5حداکثر ۵ تلاش دوباره