קונטיינר שמופעל מחדש שוב ושוב: מוצאים את הסיבה לפי קוד היציאה

בדיקה צעד אחר צעד לקונטיינר Docker שתקוע בלולאת restart: קוראים את הסטטוס ואת קוד היציאה, הולכים בענף שמתאים, ומתקנים את הסיבה בפקודות שמריצים על השרת שלכם.

פורסם ב-15 באוקטובר 2026

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

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

מוצאים את הסיבה, צעד אחר צעד

בפקודות, החליפו את <container>, ‏<service> ו-<image> בשמות שלכם. שום דבר לא רץ מהעמוד הזה.

שלב 1

באיזה מצב הקונטיינר?

מתחילים ממה ש-Docker עצמו מדווח. קונטיינר בלולאה מציג Restarting, או זמן Up שלא עולה מעבר לכמה שניות; קונטיינר שרץ אבל נכשל בבדיקת התקינות מציג (unhealthy).

החליפו את <container> בשם הקונטיינר, כפי ש-docker ps -a מציג אותו. ב-Compose זה בדרך כלל שם הפרויקט, שם השירות ומספר.

הפקודה הזו רק קוראת.

להרצה בשרת שלכם
docker ps -a --filter name=<container> --format "{{.Names}}: {{.Status}}"
מה הפלט מראה?

כל השלבים כטקסט

אותו עץ כמו באשף שלמעלה, כל הענפים בבת אחת, לקריאה או להדפסה.

הצגת כל השאלות, הבדיקות והפתרונות של האשף

באיזה מצב הקונטיינר?

מתחילים ממה ש-Docker עצמו מדווח. קונטיינר בלולאה מציג Restarting, או זמן Up שלא עולה מעבר לכמה שניות; קונטיינר שרץ אבל נכשל בבדיקת התקינות מציג (unhealthy).

החליפו את <container> בשם הקונטיינר, כפי ש-docker ps -a מציג אותו. ב-Compose זה בדרך כלל שם הפרויקט, שם השירות ומספר.

פקודה

docker ps -a --filter name=<container> --format "{{.Names}}: {{.Status}}"

תשובות

  • Restarting, או Up לכמה שניות בלבד בכל פעם: עוברים אל “קוראים את קוד היציאה”
  • Exited, והוא נשאר למטה: עוברים אל “קוראים את קוד היציאה”
  • Up, אבל מסומן (unhealthy): עוברים אל “קוראים את תוצאות בדיקת התקינות”
  • Up כבר כמה דקות ויציב כרגע: עוברים אל “אף ענף לא התאים: אוספים את מה שצריך כדי להמשיך לחפור”

קוראים את קוד היציאה

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

הפקודה מדפיסה שלושה ערכים: קוד היציאה, האם הקרנל הפסיק אותו בגלל חריגה ממגבלת הזיכרון (OOMKilled), וכמה פעמים Docker הפעיל אותו מחדש. טבלת ההסבר בהמשך העמוד מפרטת כל קוד ברשימה.

פקודה

docker inspect --format "{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}" <container>

תשובות

  • 0: עוברים אל “התהליך הראשי מסתיים ויוצא”
  • 1, או קוד אחר בין 2 ל-124, או 128: עוברים אל “קוראים את השורות האחרונות בלוג”
  • 125: עוברים אל “Docker לא הצליח להפעיל את הקונטיינר”
  • 126: עוברים אל “פקודת ההפעלה אינה ניתנת להרצה”
  • 127: עוברים אל “פקודת ההפעלה לא קיימת ב-image”
  • 137, ו-OOMKilled הוא true: עוברים אל “הקונטיינר הגיע למגבלת הזיכרון שלו”
  • 137, ו-OOMKilled הוא false: עוברים אל “מתי הוא הופסק?”
  • 139: עוברים אל “התוכנה קרסה בקוד native”
  • 143: עוברים אל “משהו מבחוץ עוצר אותו שוב ושוב”
  • קוד אחר מעל 128: עוברים אל “התהליך הופסק על ידי signal”

קוראים את השורות האחרונות בלוג

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

אם הלוג ארוך, חפשו את השורה האחרונה שמזכירה שגיאה, כישלון או דחייה, ובחרו את התשובה שמתארת אותה הכי טוב.

פקודה

docker logs --tail 50 <container>

תשובות

  • הגדרה, משתנה או קובץ הגדרות שחסרים או לא תקינים: עוברים אל “הגדרה חסרה או שגויה”
  • Connection refused, ‏timeout או host לא מוכר בפנייה למסד נתונים או לשירות אחר: עוברים אל “הוא לא מצליח להגיע לשירות שהוא תלוי בו”
  • Permission denied על קובץ או תיקייה: עוברים אל “למשתמש של הקונטיינר אין גישה לקבצים שלו”
  • Address already in use: עוברים אל “הפורט שהוא מאזין עליו כבר תפוס”
  • שום דבר ברור, או שאין לוג בכלל: עוברים אל “אף ענף לא התאים: אוספים את מה שצריך כדי להמשיך לחפור”

מתי הוא הופסק?

קוד 137 בלי OOMKilled פירושו SIGKILL שלא הגיע ממגבלת הזיכרון של הקונטיינר עצמו. יש שני מקורות נפוצים: Docker עצמו, כשקונטיינר מתעלם מבקשת העצירה עד שהזמן נגמר, והקרנל, כשלשרת כולו נגמר הזיכרון.

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

תשובות

  • בזמן שהוא נעצר, עודכן או נוצר מחדש: עוברים אל “הקונטיינר לא נעצר בזמן ומופסק בכוח”
  • מעצמו, בזמן שהוא רץ: עוברים אל “האם לשרת נגמר הזיכרון?”

האם לשרת נגמר הזיכרון?

כשלשרת כולו נגמר הזיכרון, ה-OOM killer של הקרנל בוחר תהליך ומפסיק אותו, בקונטיינר או מחוצה לו. הדגל OOMKilled של הקונטיינר נשאר false, כי המגבלה שלו לא הושגה; זה מופיע בלוג של הקרנל.

קריאת לוג הקרנל עשויה לדרוש sudo. הפקודה רק קוראת.

פקודה

journalctl -k --since "1 hour ago" | grep -i -E "out of memory|killed process"

תשובות

  • שורות עם Out of memory או Killed process: עוברים אל “לשרת נגמר הזיכרון”
  • כלום: עוברים אל “אף ענף לא התאים: אוספים את מה שצריך כדי להמשיך לחפור”

קוראים את תוצאות בדיקת התקינות

בדיקת תקינות היא פקודה ש-Docker מריץ בתוך הקונטיינר לפי לוח זמנים. ‏Docker לבדו לא מפעיל מחדש קונטיינר כי הוא unhealthy: הוא רק מסמן אותו. כשקונטיינר unhealthy מופעל מחדש, משהו אחר פועל לפי הסימון הזה, למשל Swarm, קונטיינר autoheal או סקריפט.

לכן השאלה היא למה הבדיקה נכשלת. ‏Docker שומר את התוצאות האחרונות יחד עם הפלט שלהן.

פקודה

docker inspect --format "{{json .State.Health}}" <container>

תשובות

  • הפלט של הבדיקה מראה שגיאה: not found, ‏connection refused או פורט שגוי: עוברים אל “בדיקת התקינות עצמה שבורה”
  • הבדיקה עוברת מאוחר יותר, אבל הקונטיינר מופעל מחדש לפני כן: עוברים אל “האפליקציה צריכה יותר זמן לעלות ממה שהבדיקה מאפשרת”

התהליך הראשי מסתיים ויוצא

קוד יציאה 0 אינו קריסה. קונטיינר רץ בדיוק כל עוד התהליך הראשי שלו רץ; כשהתהליך הזה מסתיים הקונטיינר נעצר, ומדיניות restart כמו always או unless-stopped מפעילה אותו שוב, בלי סוף.

שני מקרים גורמים לזה. משימה חד-פעמית (migration, גיבוי, שלב אתחול) שקיבלה מדיניות restart שמתאימה לשרתים. או שרת שמעביר את עצמו לרקע, כך שהתהליך שבחזית, שאותו Docker עוקב, יוצא מיד.

מה לעשות

  1. בדקו מה הקונטיינר מריץ.

    פקודה

    docker inspect --format "{{json .Config.Entrypoint}} {{json .Config.Cmd}}" <container>
  2. במשימה חד-פעמית, הגדירו restart: "no" או restart: on-failure לשירות הזה בקובץ ה-Compose, כך שריצה מוצלחת תישאר גמורה.
  3. בשרת, הריצו אותו בחזית: רוב ה-images מתעדים דגל לזה (ל-nginx, למשל, יש -g 'daemon off;'). אל תשאירו את הקונטיינר חי עם tail -f או sleep: התהליך האמיתי עדיין לא יהיה במעקב.
  4. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

Docker לא הצליח להפעיל את הקונטיינר

קוד 125 מגיע מ-Docker ולא מהאפליקציה: התהליך שבפנים לא עלה בכלל. הסיבה נמצאת בהודעת השגיאה של Docker, שלולאת restart מסתירה כי אף אחד לא צופה בהפעלה.

הסיבות הנפוצות הן פורט בשרת שתהליך אחר כבר מחזיק, bind mount שנתיב המקור שלו לא קיים בשרת הזה, התקן או אפשרות שהשרת לא תומך בהם, או רשת שנמחקה.

מה לעשות

  1. הפעילו את השירות פעם אחת בחזית וקראו את השגיאה ש-Docker מדפיס. עצרו אותו אחר כך עם Ctrl+C.

    משנה את השרת

    docker compose up <service>
  2. אם ההודעה לא מספיקה, קראו את הלוג של שירות Docker עצמו.

    פקודה

    journalctl -u docker --since "1 hour ago"
  3. תקנו את מה שההודעה מציינת: פורט אחר בשרת, נתיב קיים ל-mount, או אפשרות שהשרת תומך בה. אחר כך הפעילו את השירות שוב ברקע.

    משנה את השרת

    docker compose up -d <service>

פקודת ההפעלה אינה ניתנת להרצה

קוד 126 אומר שהקובץ שהקונטיינר מתחיל בו נמצא, אבל לא ניתן היה להריץ אותו. ברוב המקרים זה סקריפט בלי הרשאת הרצה: הוא איבד אותה בדרך דרך Git, ארכיון או העתקה מ-Windows, או שהוא מגיע מ-bind mount שהקובץ שלו בשרת מעולם לא קיבל הרשאת הרצה.

קובץ ב-bind mount שומר את ההרשאות שיש לו בשרת, בלי קשר למה שה-image היה מגדיר.

מה לעשות

  1. בדקו באיזה קובץ הקונטיינר מתחיל.

    פקודה

    docker inspect --format "{{json .Config.Entrypoint}} {{json .Config.Cmd}}" <container>
  2. אם הוא מגיע מ-bind mount, בדקו את ההרשאות שלו בשרת: בעמודה הראשונה צריך להופיע x.

    פקודה

    ls -l ./entrypoint.sh
  3. תנו לו הרשאת הרצה בשרת.

    משנה את השרת

    chmod +x ./entrypoint.sh
  4. אם הסקריפט נבנה לתוך ה-image, הגדירו את ההרשאות בזמן ה-build עם COPY --chmod=755 ב-Dockerfile, ובנו את ה-image מחדש.
  5. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

פקודת ההפעלה לא קיימת ב-image

קוד 127 הוא "command not found" של ה-shell. הפקודה או הסקריפט שהקונטיינר מתחיל בהם לא נמצאים איפה שההגדרות אומרות, או שהתוכנה שהם צריכים לא קיימת ב-image.

שלוש סיבות מכסות את רוב המקרים: נתיב או שם שנכתבו אחרת בקובץ ה-Compose מאשר ב-image, כלי כמו bash או curl שאין ב-image קטן יותר (Alpine, ‏slim, ‏distroless), וסקריפט שנשמר עם סיומות שורה של Windows, שהשורה הראשונה שלו מפנה אז ל-interpreter שלא קיים (/bin/sh ואחריו תו carriage return בלתי נראה).

מה לעשות

  1. בדקו את הפקודה המדויקת שהקונטיינר מתחיל בה, והשוו אותה לתיעוד של ה-image.

    פקודה

    docker inspect --format "{{json .Config.Entrypoint}} {{json .Config.Cmd}}" <container>
  2. בדקו אם בסקריפט שמועבר ב-mount או מועתק יש סיומות שורה של Windows: הפלט יכתוב אז with CRLF line terminators.

    פקודה

    file ./entrypoint.sh
  3. המירו אותו לסיומות שורה של Unix, ושמרו על כך ב-Git (‏.gitattributes עם *.sh text eol=lf).

    משנה את השרת

    sed -i 's/\r$//' ./entrypoint.sh
  4. אם הסקריפט צריך bash וב-image יש רק sh, התחילו אותו ב-#!/bin/sh והימנעו מתחביר שקיים רק ב-bash, או השתמשו בגרסת ה-image שכוללת bash.
  5. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

הקונטיינר הגיע למגבלת הזיכרון שלו

OOMKilled: true היא התשובה הברורה ביותר ש-Docker נותן: הקונטיינר השתמש ביותר זיכרון ממה שהמגבלה שלו מתירה, והקרנל הפסיק את התהליך הראשי שלו. מדיניות ה-restart מפעילה אותו שוב, הוא גדל שוב, והלולאה חוזרת, הרבה פעמים במרווח קבוע.

או שהמגבלה נמוכה ממה שהאפליקציה באמת צריכה, או שהאפליקציה מוגדרת להשתמש ביותר מהמגבלה (cache של מסד נתונים, heap של Java או מספר workers שהוגדרו לשרת שלם).

מה לעשות

  1. בדקו את מגבלת הזיכרון ב-bytes (‏0 אומר שאין לו מגבלה משלו).

    פקודה

    docker inspect --format "{{.HostConfig.Memory}}" <container>
  2. עקבו אחרי צריכת הזיכרון שלו בשניות שבהן הוא רץ.

    פקודה

    docker stats --no-stream <container>
  3. או שתעלו את המגבלה בקובץ ה-Compose (‏mem_limit, או deploy.resources.limits.memory), או שתורידו את מה שהאפליקציה מוגדרת להשתמש בו, לפי התיעוד שלה. השאירו מגבלה: היא מגינה על שאר השרת.
  4. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

הקונטיינר לא נעצר בזמן ומופסק בכוח

docker stop שולח SIGTERM, מחכה (‏10 שניות כברירת מחדל) ואז שולח SIGKILL. תהליך שלא מגיב ל-SIGTERM בזמן מסתיים ב-137 בכל עצירה, עדכון או יצירה מחדש, וכל ניקוי שהיה צריך לעשות (סגירת קבצים, סיום כתיבות) לא קורה.

הסיבה הנפוצה היא פקודת הפעלה בצורת shell: ה-shell הופך לתהליך הראשי ולא מעביר את ה-signal לאפליקציה. כיבוי איטי אבל תקין הוא הסיבה האחרת.

מה לעשות

  1. כתבו את פקודת ההפעלה בצורת exec, ‏["app", "--flag"], במקום מחרוזת רגילה, ב-Dockerfile או ב-command:.
  2. אם ה-image עולה דרך סקריפט, הוסיפו init: true לשירות, כך שתהליך init קטן יעביר signals ויאסוף תהליכי בן.
  3. אם האפליקציה צריכה יותר זמן כדי להיסגר בצורה תקינה, תנו לה זמן עם stop_grace_period: 30s.
  4. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

לשרת נגמר הזיכרון

הקרנל הפסיק תהליך בקונטיינר כי לשרת כולו לא נשאר זיכרון, ולא כי הקונטיינר הזה עבר מגבלה משלו. הקונטיינר שנופל אינו בהכרח זה שהשתמש בהכי הרבה: הקרנל בוחר לפי ניקוד משלו.

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

מה לעשות

  1. בדקו כמה זיכרון ו-swap יש לשרת, וכמה מהם פנויים.

    פקודה

    free -m
  2. בדקו אילו קונטיינרים משתמשים בהכי הרבה.

    פקודה

    docker stats --no-stream
  3. הגדירו לכל שירות מגבלת זיכרון בקובץ ה-Compose (‏mem_limit, או deploy.resources.limits.memory) כך שהסכום יהיה קטן ממה שיש לשרת, עם מקום למערכת עצמה.
  4. אם הסך הכול באמת לא נכנס, עברו לשרת עם יותר זיכרון, או העבירו שירות לשרת אחר. ‏swap יכול לספוג שיאים קצרים, אבל לא מחסור קבוע.
  5. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

התוכנה קרסה בקוד native

קוד 139 הוא segmentation fault: הקרנל עצר את התוכנה כי היא נגעה בזיכרון שאסור היה לה. הגדרות גורמות לזה לעתים רחוקות; הקובץ הבינארי עצמו גורם. בקונטיינרים הסיבות הנפוצות הן image שנבנה לסוג מעבד אחר ורץ באמולציה, build שדורש פקודות מעבד שאין בשרת הזה (יש מסדי נתונים וסביבות AI שדורשים AVX או AVX2), או באג בגרסה המדויקת.

גרסה שרצה אתמול וקורסת אחרי עדכון מצביעה על העדכון.

מה לעשות

  1. בדקו את סוג המעבד של השרת.

    פקודה

    uname -m
  2. בדקו לאיזו פלטפורמה ה-image נבנה: היא צריכה להתאים (‏x86_64 הוא amd64, ‏aarch64 הוא arm64).

    פקודה

    docker image inspect --format "{{.Os}}/{{.Architecture}}" <image>
  3. אם התיעוד של האפליקציה דורש AVX2, בדקו שיש אותו במעבד (אם אין פלט, אין).

    פקודה

    grep -o -w -m1 avx2 /proc/cpuinfo
  4. קבעו בקובץ ה-Compose את התג של הגרסה הקודמת במקום latest, ודווחו לפרויקט על הקריסה עם מספר הגרסה.
  5. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

משהו מבחוץ עוצר אותו שוב ושוב

קוד 143 אומר שהתהליך קיבל SIGTERM ועצר כפי שהתבקש. זו עצירה נקייה, לא קריסה. בלולאה, השאלה היא מי שולח אותו: קונטיינר autoheal שפועל לפי בדיקת תקינות שנכשלה, כלי עדכון כמו Watchtower, סקריפט מתוזמן, יחידת systemd, או פריסה שיוצרת את השירות מחדש שוב ושוב.

יומן האירועים של Docker רושם כל עצירה עם הזמן שלה, וזה מצמצם את החיפוש מהר.

מה לעשות

  1. הציגו את האירועים של הקונטיינר בשעה האחרונה: ‏kill, ‏stop ו-die מראים מתי הוא נעצר.

    פקודה

    docker events --since 1h --until "$(date +%s)" --filter container=<container>
  2. בדקו אילו קונטיינרים נוספים רצים בשרת: autoheal, ‏Watchtower או כלי דומה הם שולחים סבירים.

    פקודה

    docker ps --format "{{.Names}} {{.Image}}"
  3. השוו את הזמנים למשימות מתוזמנות בשרת.

    פקודה

    systemctl list-timers --all
  4. תקנו את מי ששולח, לא את הקונטיינר: תקנו את בדיקת התקינות שהוא מגיב לה, או שנו את לוח הזמנים של הכלי.

התהליך הופסק על ידי signal

כל קוד יציאה מעל 128 הוא 128 ועוד מספר ה-signal שהפסיק את התהליך. החסירו 128 וחפשו את המספר בטבלה בהמשך העמוד, או ברשימת ה-signals של Linux.

134 ‏(SIGABRT) הוא הנפוץ כאן: התוכנה עצרה את עצמה אחרי שזיהתה בעיה שלא יכלה להתאושש ממנה, ובדרך כלל כתבה על כך שורה בלוג ממש לפני. ‏130 ‏(SIGINT) אומר שהיא נקטעה, כמו ב-Ctrl+C.

מה לעשות

  1. קראו את השורות האחרונות בלוג כדי למצוא את ההודעה של התוכנה עצמה לפני ה-signal.

    פקודה

    docker logs --tail 50 <container>
  2. ב-SIGABRT, חפשו את ההודעה הזו ואת הגרסה שלכם במעקב הבאגים של הפרויקט.

הגדרה חסרה או שגויה

אפליקציות רבות בודקות את ההגדרות שלהן בעלייה ויוצאות עם שגיאה כשהגדרה נדרשת חסרה. ב-Compose הסיבות הנפוצות הן קובץ .env שלא נמצא ליד קובץ ה-Compose או שלא נקרא על ידו, משתנה שנכתב בקובץ אחרת ממה שהאפליקציה מצפה, וקובץ הגדרות ב-mount שלא קיים.

באחרון יש מלכודת: כשהמקור של bind mount לא קיים, Docker יוצר במקומו תיקייה ריקה, והאפליקציה מוצאת תיקייה במקום שבו ציפתה לקובץ.

מה לעשות

  1. הציגו את קובץ ה-Compose כפי ש-Docker קורא אותו, עם כל המשתנים ממולאים. הפלט כולל את הסודות שלכם: אל תדביקו אותו במקום ציבורי.

    פקודה

    docker compose config
  2. השוו את שמות המשתנים לתיעוד של האפליקציה, אות אחר אות.
  3. בדקו שקובץ הגדרות ב-mount הוא אכן קובץ: ‏d בתחילת השורה אומר ש-Docker יצר במקומו תיקייה.

    פקודה

    ls -ld ./config.yml
  4. אם זו תיקייה ריקה, מחקו אותה ושימו את הקובץ האמיתי במקומה.

    משנה את השרת

    rmdir ./config.yml
  5. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

הוא לא מצליח להגיע לשירות שהוא תלוי בו

האפליקציה עולה, מנסה להתחבר למסד הנתונים שלה או לשירות אחר, נכשלת ויוצאת; מדיניות ה-restart מנסה שוב. יש שתי סיבות אופייניות. הכתובת שגויה: בתוך קונטיינר, localhost הוא הקונטיינר עצמו, ולשירות אחר מגיעים לפי שם השירות שלו ב-Compose. או שהסדר שגוי: הקונטיינר של מסד הנתונים עלה, אבל עדיין לא מוכן לקבל חיבורים.

depends_on לבדו מחכה רק שהקונטיינר האחר יעלה. כדי לחכות שהוא יהיה מוכן צריך בדיקת תקינות בשירות התלות ואת התנאי service_healthy.

מה לעשות

  1. ודאו שהגדרות החיבור משתמשות בשם של השירות האחר ב-Compose (למשל db:5432), ולא ב-localhost או בכתובת השרת.
  2. קראו את הלוג של שירות התלות עצמו: ייתכן שגם הוא נכשל.

    פקודה

    docker compose logs --tail 50 <database-service>
  3. בדקו ששני הקונטיינרים נמצאים באותה רשת.

    פקודה

    docker inspect --format "{{json .NetworkSettings.Networks}}" <container>
  4. הגדירו לשירות התלות healthcheck וגרמו לאפליקציה לחכות לו עם depends_on: { db: { condition: service_healthy } }.
  5. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

למשתמש של הקונטיינר אין גישה לקבצים שלו

images רבים מריצים את האפליקציה כמשתמש שאינו root, עם מזהה מספרי קבוע. תיקייה ב-bind mount שומרת את הבעלים שיש לה בשרת, והקונטיינר רואה מספרים ולא שמות: אם התיקייה שייכת למזהה אחר, האפליקציה לא יכולה לכתוב אליה ויוצאת.

זה מופיע הרבה אחרי העברת נתונים משרת אחר, שחזור גיבוי, או יצירת התיקייה ידנית כ-root.

מה לעשות

  1. בדקו כאיזה משתמש הקונטיינר רץ (ריק אומר ברירת המחדל של ה-image; התיעוד שלו מציין את המזהה).

    פקודה

    docker inspect --format "{{.Config.User}}" <container>
  2. בדקו את הבעלים המספרי של התיקייה ב-mount בשרת.

    פקודה

    ls -ln ./data
  3. העבירו את התיקייה לבעלות המזהה של משתמש הקונטיינר (‏1000 כאן הוא דוגמה: השתמשו במזהה מהצעדים הקודמים). אל תשתמשו ב-chmod 777: הוא פותח את הנתונים לכל משתמש בשרת.

    משנה את השרת

    sudo chown -R 1000:1000 ./data
  4. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

הפורט שהוא מאזין עליו כבר תפוס

בתוך ה-network namespace שלו, קונטיינר כמעט לא פוגש מאזין אחר. לכן "Address already in use" אומר בדרך כלל אחד משני דברים: השירות רץ עם network_mode: host, כך שהוא חולק את הפורטים של השרת ותוכנה אחרת כבר מחזיקה אחד מהם; או ששני תהליכים באותו קונטיינר מנסים לפתוח את אותו פורט.

התנגשות בפורט שמפורסם (ports:) היא מקרה אחר: Docker מדווח עליה בהפעלת הקונטיינר, עם קוד יציאה 125.

מה לעשות

  1. בדקו איזו תוכנה מאזינה על כל פורט בשרת (‏sudo מציג את שמות התהליכים).

    פקודה

    sudo ss -ltnp
  2. עם network_mode: host, העבירו את האפליקציה לפורט פנוי בהגדרות שלה, או עצרו את התוכנה שמחזיקה אותו. אם אין סיבה לרשת host, העדיפו את רשת ברירת המחדל עם ports:.
  3. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

בדיקת התקינות עצמה שבורה

ייתכן שהאפליקציה תקינה והבדיקה שלה היא שנכשלת. הבדיקה רצה בתוך הקונטיינר, ולכן היא יכולה להשתמש רק בכלים שיש ב-image (בהרבה images קטנים אין curl וגם לא wget), והיא צריכה לפנות לפורט של הקונטיינר עצמו, לא לפורט שמפורסם בשרת.

תיקון הבדיקה עוצר גם את מה שמפעיל את הקונטיינר מחדש בגללה.

מה לעשות

  1. הציגו את הבדיקה כפי שהקונטיינר מריץ אותה.

    פקודה

    docker inspect --format "{{json .Config.Healthcheck}}" <container>
  2. תקנו את healthcheck.test בקובץ ה-Compose: כלי שקיים ב-image, ‏localhost, והפורט שהאפליקציה מאזינה עליו בתוך הקונטיינר. יש images שמגיעים עם פקודת בדיקה משלהם; התיעוד שלהם מציין אותה.
  3. מצאו מה מפעיל מחדש קונטיינרים unhealthy בשרת הזה (קונטיינר autoheal, ‏Swarm או סקריפט).

    פקודה

    docker ps --format "{{.Names}} {{.Image}}"
  4. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

האפליקציה צריכה יותר זמן לעלות ממה שהבדיקה מאפשרת

יש אפליקציות שצריכות כמה דקות בהפעלה הראשונה: migration של מסד נתונים, בניית אינדקס, הורדה גדולה. בינתיים הבדיקה נכשלת, הניסיונות נגמרים, הקונטיינר מסומן unhealthy ומופעל מחדש, והעבודה מתחילה מהתחלה, כך שהיא אף פעם לא מסתיימת.

start_period נותן לאפליקציה זמן חסד שבו בדיקות שנכשלות לא נספרות.

מה לעשות

  1. בדקו כמה זמן לקח לאפליקציה להיות מוכנה, לפי חותמות הזמן בלוג שלה.

    פקודה

    docker logs --timestamps --tail 100 <container>
  2. הגדירו ב-healthcheck של השירות start_period ארוך מזה (למשל start_period: 5m), והשאירו את interval ואת retries כפי שהם.
  3. צרו את הקונטיינר מחדש כדי שהשינוי ייכנס לתוקף.

    משנה את השרת

    docker compose up -d <service>

אף ענף לא התאים: אוספים את מה שצריך כדי להמשיך לחפור

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

כל הפקודות כאן רק קוראות. ‏docker inspect מדפיס את משתני הסביבה של הקונטיינר, שהרבה פעמים מכילים סיסמאות ו-tokens: הסירו אותם לפני ששולחים את הפלט למישהו.

מה לעשות

  1. הסטטוס של הקונטיינר.

    פקודה

    docker ps -a --filter name=<container>
  2. ההגדרות והמצב המלאים שלו (מסכו סודות לפני שיתוף).

    פקודה

    docker inspect <container>
  3. הלוג שלו עם חותמות זמן.

    פקודה

    docker logs --timestamps --tail 200 <container>
  4. האירועים שלו בשעה האחרונה.

    פקודה

    docker events --since 1h --until "$(date +%s)" --filter container=<container>
  5. גרסת Docker.

    פקודה

    docker version
  6. זיכרון ודיסק בשרת: גם דיסק מלא עוצר קונטיינרים.

    פקודה

    free -m && df -h
  7. לוג הקרנל של אותה שעה.

    פקודה

    journalctl -k --since "1 hour ago"

טבלת קודי יציאה

הקודים שבהם התהליך הראשי של קונטיינר מסתיים, כפי ש-docker inspect מדווח עליהם ב-.State.ExitCode. האשף שלמעלה מוביל לאותם הסברים.

קודמה הוא אומרמה לבדוק קודם
0הסתיים בהצלחה
התהליך הראשי הסתיים מעצמו ודיווח על הצלחה. שום דבר לא קרס: לתוכנה פשוט לא נשאר מה לעשות, או שהיא העבירה את עצמה לרקע והתהליך שבחזית יצא.
האם זו משימה חד-פעמית שרצה תחת מדיניות restart, או שרת שהופעל במצב daemon? קונטיינר חי רק כל עוד התהליך הראשי שלו נשאר בחזית.
1שגיאת אפליקציה
קוד הכישלון הכללי: האפליקציה עצמה החליטה לעצור כי משהו לא היה תקין. Docker לא מוסיף כאן מידע; הסיבה נמצאת בפלט של האפליקציה.
קראו את השורות האחרונות בלוג של הקונטיינר: הגדרה חסרה, חיבור שנדחה או שגיאת הרשאות מופיעים בדרך כלל ממש לפני היציאה.
125Docker לא הצליח להריץ את הקונטיינר
השגיאה הגיעה מ-Docker ולא מהאפליקציה: הקונטיינר לא הופעל בכלל, למשל בגלל אפשרות לא תקינה, פורט שכבר תפוס או התקן או mount שלא קיימים.
קראו את השגיאה ש-Docker הדפיס בהפעלת הקונטיינר, ואת הלוג של שירות Docker עצמו.
126אי אפשר להריץ את הפקודה
הפקודה שהקונטיינר מתחיל בה קיימת אבל אי אפשר להריץ אותה: לקובץ אין הרשאת הרצה, הוא תיקייה, או שלתהליך אין הרשאה להריץ אותו.
בדקו את ה-entrypoint וה-command של הקונטיינר ואת ההרשאות של הסקריפט שהם מצביעים עליו, במיוחד כשהוא מגיע מ-bind mount.
127הפקודה לא נמצאה
הפקודה שהקונטיינר מתחיל בה לא קיימת בתוך ה-image: נתיב שגוי, כלי שחסר ב-image מצומצם יותר, או סקריפט שהשורה הראשונה שלו מפנה ל-interpreter שלא קיים.
השוו את הפקודה בקובץ ה-Compose או ב-Dockerfile למה שבאמת יש ב-image, וחפשו סיומות שורה של Windows בסקריפטים של shell.
128ערך יציאה לא תקין
128 עצמו אינו signal. יש תוכנות שמסתיימות בו בשגיאה קריטית בעלייה, ו-shells משתמשים בו כשסקריפט קורא ל-exit עם ערך שאי אפשר להשתמש בו. קודים מעל 128 שונים: הם אומרים שהתהליך הופסק על ידי signal.
התייחסו אליו כמו לשגיאת אפליקציה: הלוג מראה מה נכשל.
137הופסק בכוח (SIGKILL), לרוב בגלל זיכרון 128 + signal 9 ‏(SIGKILL)
128 + 9: התהליך הופסק עם SIGKILL ולא הספיק לסגור את עצמו. ה-OOM killer של הקרנל עושה זאת כשקונטיינר עובר את מגבלת הזיכרון שלו או כשלשרת נגמר הזיכרון; גם Docker עושה זאת כשקונטיינר מתעלם מבקשת העצירה עד שהזמן נגמר.
בדקו את .State.OOMKilled ב-docker inspect, ואחר כך את לוג הקרנל של השרת.
139Segmentation fault ‏(SIGSEGV) 128 + signal 11 ‏(SIGSEGV)
128 + 11: התוכנה קראה או כתבה לזיכרון שאסור היה לה לגשת אליו, והקרנל עצר אותה. בקונטיינר זה מצביע בדרך כלל על קוד native: ספרייה שלא מתאימה ל-image, build שדורש יכולות מעבד שאין בשרת הזה, או באג בגרסה הזו.
השוו את הארכיטקטורה של ה-image לזו של השרת, ונסו את הגרסה הקודמת של ה-image.
143נעצר לפי בקשה (SIGTERM) 128 + signal 15 ‏(SIGTERM)
128 + 15: משהו ביקש מהתהליך לעצור והוא עצר. זו התוצאה הרגילה של docker stop או של יצירה מחדש של קונטיינר; בלולאה זה אומר שמשהו מחוץ לקונטיינר עוצר אותו שוב ושוב.
מצאו מי שולח את העצירה: אירועי Docker של הקונטיינר מראים כל עצירה ומה הפעיל אותה.

כל קוד אחר מעל 128 הוא 128 ועוד מספר ה-signal שהפסיק את התהליך. הנפוצים: 129 = SIGHUP, 130 = SIGINT, 131 = SIGQUIT, 132 = SIGILL, 134 = SIGABRT, 135 = SIGBUS, 136 = SIGFPE, 141 = SIGPIPE, 142 = SIGALRM.

מה לא מכוסה כאן

האשף מכסה שרת Docker יחיד עם Docker Engine ו-Compose. ‏Kubernetes ‏(CrashLoopBackOff), שירותי Swarm ו-Podman מדווחים על אותם קודי יציאה, אבל הפקודות ולוגיקת ההפעלה מחדש שלהם שונות.

פקודות שמסומנות כמשנות את השרת עושות בדיוק את זה: קראו אותן לפני שאתם מריצים, ושמרו גיבוי של כל תיקיית נתונים שאתם משנים. כשאף ענף לא מתאים, השלב האחרון מפרט מה לאסוף; הוא לא מנחש.

מקורות

האופן שבו ANAROQ מתעד מקורות לעובדות שבעמודים שלו מתואר במתודולוגיה. למתודולוגיה

כל עמודי פתרון התקלות