SSD Nodes Learn 🎉 VPS החל מ־$5.50/חודש
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-13

ההבדל בין command ל-entrypoint ב-Docker Compose

למדו איך לעבוד עם ENTRYPOINT ו-command ב-Docker Compose. המדריך מסביר מדוע הגדרת ENTRYPOINT מוחקת את ה-CMD המקורי ומציג את ארבעת השילובים האפשריים לעקיפת הגדרות האימג'.

ההבדל בין command ל-entrypoint ב-Docker Compose, בכלל אחד

ב-Docker Compose, המפתח entrypoint: מגדיר את התוכנית שתרוץ, והמפתח command: מגדיר את הארגומנטים שיועברו לתוכנית זו. התהליך של המכולה הוא רשימת ה-entrypoint בצירוף רשימת ה-command בסופה. כל התנהגות אחרת המתוארת בדף זה נובעת ממשפט יחיד זה.

שני המפתחות הללו ממופים לשתי הוראות ב-Dockerfile. המפתח entrypoint: מחליף את ה-ENTRYPOINT של האימג'. המפתח command: מחליף את ה-CMD של האימג'. הם אינם בלתי תלויים, וזהו המקום שבו משתמשים נתקלים בקשיים: הגדרת entrypoint: מוחקת גם את ה-CMD של האימג'. מפרט ה-Compose מציין זאת במפורש. אם entrypoint אינו null, ה-Compose מתעלם מכל פקודת ברירת מחדל שמגיעה מהאימג'.

קרא את מה שהאימג' כבר מצהיר עליו

לפני שאתה דורס הגדרה כלשהי, בדוק מה האימג' מספק.

docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16

אתה מקבל את ["docker-entrypoint.sh"] ואת ["postgres"], כך שהקונטיינר מריץ את docker-entrypoint.sh postgres. הסקריפט הזה יוצר את ספריית הנתונים בעלייה הראשונה, קורא את המשתנים ב-POSTGRES_*, מוריד הרשאות למשתמש postgres, ולבסוף מריץ את הארגומנטים שניתנו לו. ההחלטה כולה תלויה בהבנה איזה חלק אתה רוצה לשנות. כדי להעביר דגל (flag) למסד הנתונים, עליך להחליף את command:. אם תחליף את entrypoint:, שום חלק מתהליך ההגדרה הזה לא ירוץ כלל.

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

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

FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]
docker build -t argdemo .
services:
  demo:
    image: argdemo

הריצו את docker compose up לאחר כל עריכה וקראו את השורה הבודדת שהוא מתעד ביומן.

  • אף מפתח לא הוגדר. התהליך הוא /bin/echo ep cmd והיומן מציג ep cmd.
  • command: ["cmd2"] בלבד. התהליך הוא /bin/echo ep cmd2. ה-entrypoint נשאר ללא שינוי ורק הארגומנטים השתנו.
  • entrypoint: ["/bin/echo", "ep2"] בלבד. התהליך הוא /bin/echo ep2 והיומן מציג ep2. ה-cmd מה-image נעלם, ושום דבר לא מזהיר אתכם מכך.
  • שני המפתחות הוגדרו. התהליך הוא /bin/echo ep2 cmd2. זהו המקרה היחיד שבו אתם שולטים בכל רשימת הארגומנטים.

מדוע הגדרת entrypoint מוחקת את ה-CMD של ה-image

ה-CMD של image נכתב כרשימת הארגומנטים המוגדרת כברירת מחדל עבור ה-ENTRYPOINT של אותו image. כאשר מחליפים את ה-entrypoint, הארגומנטים הללו משויכים לתוכנית שכבר אינה רצה, ולכן Docker Compose משמיט אותם במקום לבנות שורת פקודה שיוצר ה-image מעולם לא התכוון שתתקיים. docker run --entrypoint מתנהג באותו אופן, כך שמדובר בהתנהגות של Docker ולא במאפיין ייחודי של Docker Compose.

התוצאה היא מוחשית. nginx:1.27 מצהיר על ENTRYPOINT ["/docker-entrypoint.sh"] ועל CMD ["nginx", "-g", "daemon off;"]. אם תגדירו entrypoint: /custom-init.sh, הסקריפט שלכם יתחיל עם רשימת ארגומנטים ריקה. סקריפט שמסתיים ב-exec "$@" הרגיל ימצא שאין לו מה להריץ (exec), לכן ה-exec לא יבצע דבר, הסקריפט יגיע לשורתו האחרונה, וה-container יסתיים עם קוד 0 ללא הודעת שגיאה בשום מקום. עליכם להחזיר את הארגומנטים בעצמכם:

services:
  web:
    image: nginx:1.27
    entrypoint: /custom-init.sh
    command: ["nginx", "-g", "daemon off;"]

הכלל שיש לזכור: בכל פעם שאתם מגדירים entrypoint:, החליטו מה צריך להיות ה-command: באותה עריכה.

פורמט Exec ופורמט Shell, וההבדלים ב-Compose

קובץ Dockerfile מקבל שתי תחבירים. CMD ["nginx", "-g", "daemon off;"] הוא פורמט exec: הבינארי רץ ישירות, ללא מעורבות של shell. CMD nginx -g "daemon off;" הוא פורמט shell: ‏Docker משכתב אותו כ-/bin/sh -c 'nginx -g "daemon off;"', כך ש-shell רץ תחילה והתוכנית שלכם הופכת לבן שלו.

‏Compose לא מעתיק את הכלל הזה, וזה מפתיע משתמשים. מחרוזת ב-command: מפוצלת לארגומנטים ומבוצעת ישירות, ללא מעטפת /bin/sh -c. התיעוד של Compose מפורש בנושא: השדה command לא רץ בתוך ההקשר של SHELL המוגדר ב-image, לכן אם אתם זקוקים לתכונות של shell, עליכם להפעיל shell בעצמכם.

זו הסיבה ש-command: echo "hello $$HOSTNAME" מדפיס את הטקסט המילולי hello $HOSTNAME. אף shell לא ראה את המחרוזת, לכן שום דבר לא ביצע לה הרחבה. בקשו shell כאשר אתם זקוקים לאחד כזה:

services:
  demo:
    image: alpine:3.20
    command: /bin/sh -c 'echo "hello $$HOSTNAME"'

אותות, PID 1, ופקודת docker compose down תקינה

docker compose stop ו-docker compose down שולחים SIGTERM ל-PID 1 בתוך כל מכולה, ממתינים stop_grace_period, ואז שולחים SIGKILL. תקופת החסד המוגדרת כברירת מחדל היא 10 שניות.

PID 1 הוא תהליך מיוחד ב-Linux. ה-kernel לא מחיל את פעולת ברירת המחדל של אות על PID 1, לכן תהליך שלא מגדיר handler ל-SIGTERM פשוט מתעלם מ-SIGTERM כשהוא רץ כ-PID 1. הוא נשאר פעיל לאורך כל תקופת החסד ואז מופסק בכוח, מה שגורם לניתוק חיבורים פתוחים או לביטול טרנזקציות שלא הושלמו.

שימוש ב-shell לפני התוכנית שלכם הופך מצב זה לסביר יותר, כיוון שה-shell הוא PID 1 ורוב ה-shells לא מעבירים אותות לתהליכי הבן שלהם. חלק מה-shells מחליפים את עצמם בפקודה הסופית במחרוזת -c, כך שלעיתים התוכנית שלכם אכן מגיעה להיות PID 1. זה תלוי ב-shell ובמחרוזת המדויקת, לכן אל תנחשו. בדקו זאת:

docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echo

אם PID 1 מציג /bin/sh -c ... במקום התוכנית שלכם, יש שני פתרונות. השתמשו ב-exec form בתוך ה-image, או השאירו את ה-shell והעבירו את התהליך באמצעות exec:

services:
  web:
    image: myapp:1.4
    command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'

exec מחליף את תהליך ה-shell בתוכנית שלכם במקום ליצור תהליך בן, כך שהתוכנית שלכם יורשת את PID 1 ומקבלת את האות.

תוכניות מסוימות יוצרות תהליכי בן ולעולם לא אוספות אותם, מה שמשאיר תהליכי זומבי, כיוון ש-PID 1 הוא גם ה-reaper. ל-Compose יש הגדרה לכך:

services:
  web:
    image: myapp:1.4
    init: true
    stop_grace_period: 30s

init: true מריץ תהליך init קטן כ-PID 1 שמעביר אותות לתהליך שלכם ואוסף תהליכי בן. stop_grace_period נותן לכיבוי איטי באמת מרחב פעולה גדול יותר. אם התוכנית שלכם מצפה לאות אחר, stop_signal: SIGQUIT משנה את האות ש-Compose שולח. בדקו מה ה-image מבקש כבר עכשיו באמצעות docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.

מערכת שבה docker compose down תמיד לוקח עשר שניות לכל שירות מאותתת לכם ששום דבר לא מטפל ב-SIGTERM. תקנו זאת לפני שתאשימו את הכלים, ועיינו ב-ההבדל בין docker compose down ל-stop כדי להבין מה כל פקודת משנה מסירה.

אותו פיצול בין exec ל-shell מופיע במקום נוסף. בדיקת תקינות (healthcheck) שנכתבת כ-test: ["CMD", "curl", "-f", "http://localhost/"] מריצה את ה-binary ישירות, בעוד ש-test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] מריצה אותו דרך shell כך ש-|| מקבל משמעות. כתיבת healthchecks ל-Compose שנכשלים בצורה אמינה מכסה את שאר הנושא.

הוספת דגל לתמונה רשמית

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

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    command: postgres -c max_connections=200 -c shared_buffers=256MB

volumes:
  pgdata:

רק command: השתנה, לכן docker-entrypoint.sh עדיין רץ ומבצע את מה שהגדרתם לו. בדקו את התוצאה במקום להניח שהיא תקינה:

docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'

הפלט אמור להציג את 200. אם הוא עדיין מציג את 100, הריצו את docker compose config וודאו שה־command שאתם מצפים לו נמצא בפלט הממוזג. קובצי override ב-Compose מחליפים את command לחלוטין, הם לא משרשרים אליו, לכן קובץ שני שמגדיר גם הוא את command: ינצח בשקט.

ה־${POSTGRES_PASSWORD} שלעיל מורחב על ידי Compose במארח מתוך קובץ ה-.env שלכם, עוד לפני שהמכולה קיימת. המדריך קובצי סביבה וסודות ב-Compose מפרט היכן ניתן לשמור ערך זה בבטחה.

הרצת פעולת מיגרציה חד-פעמית באמצעות docker compose run

docker compose run בונה מכולה חדשה על בסיס אותה הגדרת שירות ומחליפה את הפקודה במה שתקלידו לאחר שם השירות. ה-entrypoint של האימג' עדיין רץ, כך שהמכולה מוכנה בדיוק כמו המכולה שרצה באופן קבוע.

docker compose run --rm app python manage.py migrate
  • --rm מוחק את המכולה עם סיום הפקודה. ללא דגל זה, כל הרצה משאירה מאחוריה מכולה עצורה, שניתן לראות ב-docker compose ps -a.
  • פורטים אינם מפורסמים. מכולה מסוג run מתעלמת מה-ports: של השירות אלא אם תוסיפו --service-ports, לכן היא לא יכולה להתנגש עם השירות שכבר פעיל.
  • תלויות עולות תחילה. כל מה שמוגדר ב-depends_on יעלה לפני הפקודה שלכם, ו---no-deps מדלג על שלב זה.
  • המכולה מקבלת שם שנוצר אוטומטית כמו myproject-app-run-9f2c1a, כך שהיא לעולם לא תתנגש עם מכולת השירות.

כדי להחליף גם את ה-entrypoint, קיים דגל ייעודי לכך:

docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'

רשימת הארגומנטים המתקבלת היא /bin/sh -c 'python manage.py migrate', כיוון שהמילים לאחר שם השירות הן עדיין הפקודה. docker compose exec הוא הכלי השני והוא עובד אחרת: הוא מריץ תהליך בתוך מכולה שכבר פעילה, והוא מתעלם לחלוטין מ-entrypoint: ומ-command:. השתמשו ב-run עבור משימה שזקוקה למכולה חדשה, וב-exec כדי להציץ לתוך מכולה פעילה. ה-דף עזר לפקודות Compose מציג את שאר תתי-הפקודות זו לצד זו.

מדוע המכולה שלי נסגרת מיד?

התחילו בבדיקת קוד היציאה (exit code), שכן הוא מצמצם את טווח הסיבות האפשריות במהירות.

docker compose ps -a
docker compose logs app

קוד יציאה 0 ללא פלט. הפקודה רצה והסתיימה. הסיבה הנפוצה ביותר היא דריסה (override) של entrypoint: שלקחה איתה את ה-CMD של האימג', כך שה-entrypoint רץ עם רשימת ארגומנטים ריקה ולא היה לו מה להריץ.

שגיאה המסתיימת ב-permission denied. לסקריפט אין הרשאת הרצה (executable bit) בתוך האימג', בדרך כלל משום שההרשאה לא הוגדרה מעולם על הקובץ במאגר (repository). הגדירו אותה בזמן הבנייה באמצעות COPY --chmod=0755 entrypoint.sh /entrypoint.sh.

שגיאה המסתיימת ב-no such file or directory עבור קובץ שניתן לראות בבירור באימג'. לסקריפט יש סיומות שורות (line endings) בסגנון Windows. השורה הראשונה שלו נקראת כ-#!/bin/sh בתוספת תו carriage return, ולכן ה-kernel מחפש מפרש (interpreter) עם התו הזה בשמו ולא מוצא דבר. הריצו dos2unix entrypoint.sh, ולאחר מכן הוסיפו את * text eol=lf ל-.gitattributes כדי למנוע מהבעיה לחזור.

executable file not found in $PATH. הקובץ הבינארי שצוין ב-command: אינו קיים באימג', או שכתבתם פקודה מובנית של ה-shell כמו cd במקום שבו יכולה להופיע רק תוכנית ממשית.

קבלת גישה ל-shell במכולה שבה ה-entrypoint נכשל

כאשר ה-entrypoint קורס לפני שניתן לבדוק דבר מה, יש להחליף אותו:

docker compose run --rm --entrypoint /bin/sh app

אם הפקודה מחזירה executable file not found in $PATH, סימן שב-image אין shell כלל. תמונות מסוג Distroless או מבוססות scratch לרוב אינן כוללות shell. עדיין ניתן לקרוא את מערכת הקבצים מבחוץ מבלי להפעיל את ה-entrypoint:

docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probe

כאשר יש צורך להשאיר את המכולה פעילה כדי להתחבר אליה שוב ושוב, יש להריץ אותה על תהליך שלעולם אינו מסתיים. יש להוסיף זאת לקובץ override שאינו נשמר ב-repository:

services:
  app:
    entrypoint: ["tail", "-f", "/dev/null"]
    command: []

השימוש ב-command: [] אינו הכרחי מבחינה טכנית, שכן הגדרת entrypoint: כבר ניקתה את ה-CMD של ה-image, אך כתיבתו מתעדת את הכוונה עבור מי שיקרא את הקובץ בעתיד. יש להעלות את המכולה ולהיכנס פנימה:

docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/sh

כעת ניתן להריץ את ה-entrypoint האמיתי ידנית ולעקוב אחר נקודת העצירה שלו. כך תתקבל הודעת השגיאה ב-terminal במקום בתוך מכולה שקרסה לפני חצי שנייה. אם אתם עדיין בונים את ה-stack הראשון שלכם, בניית Compose stack ראשון על גבי VPS מכסה את מבנה הקבצים שעליו מתבסס כל האמור לעיל.

FAQ

Why does my container exit immediately after docker compose up?

Check docker compose ps -a for the exit code. Exit 0 with no output usually means you set entrypoint: on the service, which also cleared the image's CMD, so the entrypoint ran with an empty argument list and finished. Add the arguments back with command:. An error ending in permission denied means the entrypoint script has no executable bit. An error ending in no such file or directory for a file that exists means the script has Windows line endings, so its shebang line names an interpreter that is not there.

Does setting entrypoint in Compose remove the image's CMD?

Yes. If entrypoint is non-null, Compose ignores any default command declared by the image. That is documented behaviour and it matches docker run --entrypoint. The reason is that an image's CMD is written as arguments for that image's ENTRYPOINT, so once you replace the entrypoint the old arguments no longer belong to anything. Set command: in the same service if the new entrypoint still needs arguments.

Is a string in Compose command run through a shell?

No. Unlike a Dockerfile CMD, a string in Compose command: is split into arguments and executed directly, with no /bin/sh -c wrapper. So $VARIABLE is never expanded by a shell inside the container. Call the shell yourself when you need one, as in command: /bin/sh -c 'echo "hello $$HOSTNAME"'. The doubled $$ escapes the dollar sign so Compose passes it through to the container instead of expanding it on the host.

Why does docker compose down take ten seconds for one container?

Compose sends SIGTERM to PID 1, waits for stop_grace_period (10 seconds by default), then sends SIGKILL. The kernel does not apply default signal actions to PID 1, so a program with no SIGTERM handler ignores the signal and always waits out the full period. Find out what PID 1 really is with docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '. If it is a shell, switch the image to exec form or write exec inside the shell string. If the process spawns children it never reaps, set init: true on the service.

#docker-compose#entrypoint#command#containers#debugging