SSD Nodes Learn Hosting plans →
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-07

איך להגדיר healthcheck ב-Docker Compose בצורה נכונה

מדריך מעשי להגדרת healthcheck ב-Docker Compose. תלמדו מדוע depends_on לא מספיק, איך עובדים קודי יציאה, ואיך לכתוב בדיקות תקינות עבור Postgres והאפליקציה שלכם בקלות.

מה באמת עושה healthcheck ב-Docker Compose

‏healthcheck ב-Docker Compose הוא פקודה בודדת ש-Docker מריץ בתוך המכולה לפי טיימר. Docker לא קורא את הלוגים שלכם, לא מנטר את הפורט ולא בודק את רשימת התהליכים. הוא מריץ את הפקודה, קורא את קוד היציאה ושומר מצב יחיד עבור המכולה: starting, healthy או unhealthy. קוד יציאה 0 משמעו תקין (healthy). כל קוד יציאה אחר משמעו לא תקין (unhealthy), וקוד יציאה 2 שמור לשימוש על ידי Docker, לכן לעולם אל תחזירו אותו בכוונה.

זהו כל המנגנון. כמעט כל בעיה ב-healthcheck נובעת מאותה סיבה: הפקודה שכתבתם עונה על שאלה שונה מזו שהתכוונתם לשאול. מדריך זה מניח שאתם כבר יודעים איך לכתוב קובץ compose ב-VPS, וממשיך מהנקודה שבה ה-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: 30s

הערך test מקבל שתי צורות שימושיות. רשימה המתחילה ב-CMD מריצה את הפקודה ישירות, ללא shell, ולכן pipes, סימני && והרחבת משתנים לא יעבדו. רשימה המתחילה ב-CMD-SHELL מעבירה את שאר התוכן כמחרוזת אחת ל-/bin/sh -c בתוך המכולה, וזה מה שתרצו בכל פעם שהבדיקה דורשת תחביר של shell. מחרוזת פשוטה מטופלת כ-CMD-SHELL. רשימה המכילה בדיוק ["NONE"] מסירה healthcheck שהוטמע ב-image דרך ה-Dockerfile שלו.

הבדיקה רצה בתוך המכולה, לכן כל קובץ בינארי שהיא מציינת חייב להיות קיים ב-image. ודאו זאת תחילה, כיוון ש-image רזה ללא curl ייצור מכולה שתהיה במצב לא תקין באופן קבוע מסיבה שלעולם לא תופיע בלוג של היישום. בדקו זאת ידנית:

docker compose exec api curl --version

קובץ בינארי חסר משיב עם OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. images מבוססי 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 מסתיימת בעוד הבדיקה עדיין נכשלת, מתחילה הספירה הרגילה, והמכולה תזדקק ל-retries כישלונות רצופים לפני שתסומן כ-unhealthy.

לכן, זמן ה"מקרה הגרוע ביותר" מרגע עליית המכולה ועד ל-unhealthy הוא start_period ועוד retries כפול interval ועוד timeout. עם הערכים בקובץ שלעיל, מדובר ב-30 ועוד 5 כפול 13, כלומר 95 שניות. רשמו מספר זה לפני הגדרת deploy timeout, שכן תהליך rollout שמוותר לאחר 60 שניות לעולם לא יראה את המכולה הזו מגיעה למצב סופי.

הטעות הנפוצה כאן היא העלאת ה-retries כדי לפצות על עלייה איטית. זה עובד פעם אחת אך מזיק לטווח ארוך: שירות שהיה זקוק ל-8 ניסיונות כדי לעלות, יסבול כעת 8 כישלונות רצופים בסביבת הייצור לפני שמישהו יבחין בכך. השתמשו ב-start_period במקום זאת, כיוון שהוא חל רק לפני ההצלחה הראשונה.

מדוע depends_on לבדו אינו מבטיח דבר

הצורה המקוצרת של depends_on היא המקור לרוב הבלבול.

  api:
    depends_on:
      - db

משמעות הדבר היא אחת: הפעל את המכולה db לפני המכולה api. ‏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_successfully

ל-condition יש שלושה ערכים. service_started זהה לצורה המקוצרת. service_healthy מעכב את השירות התלוי עד שהתלות מדווחת על מצב תקין (healthy), דבר שרלוונטי רק כאשר התלות מגדירה healthcheck, בין אם בקובץ ה-compose או בתוך ה-image שלה. service_completed_successfully ממתין למכולה מסוג one-shot, כמו הרצת migration למסד נתונים, עד שתסיים עם סטטוס 0.

לצד condition קיימים שני שדות נוספים. restart: true מורה ל-Compose להפעיל מחדש את השירות לאחר עדכון שירות התלות. required: false מוריד את דרגת החומרה של תלות חסרה משגיאה לאזהרה.

כעת, המגבלה שתופסת משתמשים רבים: תנאים אלו נבדקים בעת עליית ה-stack. מדובר בסדר הפעלה, לא בכלל ניטור. אם מסד הנתונים יקרוס בשלוש לפנות בוקר, שום דבר לא יבצע הערכה מחדש ל-service_healthy ושום דבר לא יפעיל מחדש את היישום שלכם כדי לספק את התלות הזו שוב. קוד היישום שלכם עדיין חייב לדעת לבצע חיבור מחדש באופן עצמאי. docker compose up --no-deps api מדלג על כל המנגנון הזה מעצם תכנונו, וכך גם הפעלת מכולה ישירות באמצעות docker start.

כתבו בדיקה שבוחנת מוכנות, לא רק קיום של תהליך

בדיקה כמו pgrep nginx מוכיחה רק שקיימת רשומה בטבלת התהליכים. היא אינה מוכיחה שהשירות מסוגל להשיב לבקשות. יישום ווב יכול להחזיק socket פתוח זמן רב לאחר שמאגר החיבורים למסד הנתונים שלו קרס, ובדיקת התהליך תמשיך להציג מצב תקין לאורך כל התקלה.

דרשו מהמכולה לבצע את העבודה שלשמה היא קיימת:

  • עבור שירות HTTP, בצעו בקשה ל-endpoint אמיתי. curl -fsS מסתיים עם קוד שגיאה (non-zero) בכל סטטוס של 400 ומעלה בזכות -f, כך שסטטוס 500 מיישום תקול ייחשב כבדיקה שנכשלה.
  • עבור PostgreSQL, השתמשו ב-pg_isready, שמחזיר 0 כאשר השרת מקבל חיבורים, 1 כאשר הוא דוחה אותם, 2 כאשר אין תגובה כלל, ו-3 כאשר הפרמטרים שהועברו שגויים.
  • עבור Redis, השתמשו ב-redis-cli ping, שמדפיס PONG ומסתיים עם קוד 0.
  • עבור MariaDB, האימג' הרשמי כולל סקריפט healthcheck.sh, ו-healthcheck.sh --connect --innodb_initialized הוא הפורמט המתועד על ידי מתחזקי האימג'.

ל-pg_isready יש מלכודת אחת שכדאי להכיר. בהפעלה הראשונה עם תיקיית נתונים ריקה, האימג' הרשמי של postgres מריץ אתחול מול שרת זמני שמאזין רק ל-Unix socket. pg_isready ללא ארגומנט host משתמש ב-socket הזה, ולכן הוא יכול להשיב שהוא "מקבל חיבורים" בעוד פורט 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 בעצמו בזמן קריאת הקובץ, מה שהיה גורם להטמעת ערך מסביבת ה-host שלכם בתוך הבדיקה. $$ מבצע escape לסימן בודד של $, כך שה-shell בתוך המכולה מרחיב אותו מול סביבת העבודה של המכולה עצמה.

מחסנית של Postgres ויישום שמתחילה בסדר הנכון

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 ps

העמודה STATUS מציגה את מצב התקינות בסוגריים. צמד תקין יציג 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 של הפקודה. הפלט השמור קטוע, לכן בדיקה שמדפיסה גוף דף גדול תניב רשומת לוג חסרת תועלת. הקפידו על בדיקות שקטות.

מה Docker עושה כאשר מכולה הופכת ל-unhealthy

שום דבר. זו התשובה שמפתיעה אנשים יותר מכל.

מנוע ה-Docker על שרת בודד אינו מבצע הפעלה מחדש למכולה במצב unhealthy. מדיניות ה-restart: unless-stopped מגיבה ליציאה של התהליך הראשי, ומכולה במצב unhealthy לא יצאה מהתהליך. היא יכולה להישאר במצב unhealthy במשך שבוע בעוד Compose מתעלם ממנה. מצב Swarm מחליף משימות לא תקינות, אך stack פשוט של Compose על שרת יחיד אינו עושה זאת.

זה מותיר שתי אפשרויות מעשיות. לגרום לתהליך לצאת כאשר הוא מזהה תקלה, כך שלמדיניות ה-restart יהיה על מה לפעול. לחלופין, לנטר את המצב מבחוץ ולקבל התראה. הפניית ניטור Uptime Kuma לאותו endpoint שבו משתמש ה-healthcheck שלכם, משמעותה שתלות שבורה תופיע בשני המקומות, ואתם תקבלו על כך התראה מהמערכת לפני שמשתמש ידווח על כך. אם התעבורה מגיעה ליישום דרך reverse proxy מסוג Traefik, זכרו שהתצוגה של ה-proxy על ה-backend נפרדת ממצב ה-health של Docker, כך שאחד אינו מכסה את השני.

ניפוי שגיאות בבדיקת תקינות (healthcheck) שאינה הופכת ל-healthy

הריצו את הפקודה המדויקת בעצמכם, בתוך אותו ה-container, ובדקו את קוד היציאה (exit code):

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

exit=0 כאן בזמן שה-container עדיין מדווח על מצב unhealthy מעיד על כך שה-test ב-compose שלכם שונה ממה שהקלדתם כרגע, בדרך כלל משום שנעשה שימוש ב-CMD במקום שבו נדרש תחביר של shell.

שתי טעויות אחראיות לרוב המקרים הנותרים. הראשונה היא פורט שגוי. ה-healthcheck רץ בתוך ה-container, לכן עליו להשתמש בפורט של ה-container, לעולם לא בפורט ה-host שפורסם. עם ports: - "8080:3000" היישום מאזין ב-3000, ובדיקה מול http://localhost:8080 תיכשל תמיד, בעוד שהאתר עובד מצוין בדפדפן. השנייה היא host שגוי. בתוך הבדיקה, localhost הוא אותו ה-container עצמו, וזה נכון לבדיקה עצמית אך שגוי לבדיקת שכן, שם עליכם להשתמש בשם השירות, למשל db.

מקרה אחרון ראוי לציון: ה-healthcheck עובר בעוד המשתמשים רואים שגיאות. זה קורה כאשר ה-endpoint מחזיר 200 סטטי מבלי לגשת לשום דבר ממשי. endpoint של readiness שלעולם לא מבצע שאילתה למסד הנתונים לא יכול להודיע לכם שמסד הנתונים אינו זמין. הגדירו אותו כך שיריץ שאילתה אחת פשוטה ואמיתית.

FAQ

מדוע היישום שלי עדיין נכשל בחיבור למרות ש-depends_on מציין שמסד הנתונים תקין?

מכיוון ש-condition: service_healthy מוערך פעם אחת בלבד, בעת הפעלת ה-stack. הוא אינו מבצע ניטור לאחר מכן. אם מכולת מסד הנתונים מאותחלת מאוחר יותר, Compose לא מאתחל את היישום שלך כדי לעמוד בתנאי שוב, לכן קוד היישום שלך חייב לכלול לוגיקה עצמאית של חיבור מחדש וניסיונות חוזרים. התנאי גם אינו משפיע כאשר מפעילים מכולה בודדת עם docker start או עם docker compose up --no-deps.

האם אני צריך healthcheck אם ה-image כבר מגדיר אחד כזה?

בדרך כלל לא, ודריסה שלו היא לרוב צעד לאחור, כיוון שמתחזק ה-image יודע מהי המשמעות של מוכנות עבור אותה תוכנה. הוסף בדיקה משלך רק כאשר הבדיקה של ה-image אינה מתאימה להגדרה שלך, למשל כאשר היא בודקת פורט שהעברת. כדי לבטל healthcheck של image, הגדר test: ["NONE"] או disable: true בשירות.

האם ה-healthcheck צריך להשתמש ב-curl או ב-wget?

השתמש בכלי שכבר קיים ב-image, ואמת זאת עם docker compose exec <service> curl --version לפני שאתה מסתמך עליו. ב-images רבים מבוססי Debian אין אף אחד מהם. ב-images מבוססי Alpine קיים wget של BusyBox. אל תוסיף חבילה ל-image רק כדי להריץ healthcheck כאשר התוכנה מספקת לקוח משלה, כגון pg_isready או redis-cli.

האם מכולה במצב unhealthy מאותחלת אוטומטית?

לא על ידי Docker Engine במארח בודד. מדיניות אתחול מגיבה ליציאת התהליך, לא למצב הבריאות, לכן מכולה לא תקינה נשארת פעילה ושבורה עד שגורם אחר פועל עליה. או שתגרום לתהליך לצאת כאשר הוא מזהה את הכשל, או שתריץ ניטור חיצוני שמתריע על המצב.

כמה זמן צריך להיות start_period?

ארוך מספיק עבור ההפעלה הראשונה והאיטית ביותר שמדדת, בתוספת מרווח ביטחון. מדוד זאת עם docker compose up מול volume ריק, כיוון שההפעלה הראשונה של מסד נתונים איטית בהרבה מכל הפעלה שאחריה. start_period ארוך מדי רק מעכב את פסק הדין הראשון של unhealthy. מספר ניסיונות חוזרים גבוה מדי מחליש את הבדיקה לאורך כל חיי המכולה, וזהו כשל חמור יותר.

#docker-compose#healthcheck#depends-on#docker#reliability