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

CMD در برابر ENTRYPOINT

توی این درس یاد می‌گیری وقتی docker run image را می‌زنی، داکر دقیقاً چه دستوری را اجرا می‌کند و کی CMD یا ENTRYPOINT جای آن را می‌گیرند: CMD یک پیش‌فرض قابل‌تغییر است و ENTRYPOINT «خود برنامه‌ی image» که آرگومان‌های docker run به آن اضافه می‌شود. فرم exec (["a","b"]) و فرم shell (a b) را از هم جدا می‌کنی، با docker run --entrypoint آن را عوض می‌کنی و می‌فهمی چرا PID 1 و سیگنال‌ها اینجا مهم‌اند.

تشبیه: دستگاه با دکمه‌ی پیش‌فرض

Section titled “تشبیه: دستگاه با دکمه‌ی پیش‌فرض”

ENTRYPOINT مثل کاری است که یک دستگاه برایش ساخته شده (مثلاً «قهوه‌ساز»). CMD مثل تنظیم پیش‌فرض آن است (مثلاً «قهوه‌ی متوسط»). وقتی docker run image دستور را می‌زنی مثل این است که تنظیم پیش‌فرض را عوض کنی: «قهوه‌ی بزرگ». ولی دستگاه همچنان قهوه‌ساز است. اگر فقط CMD داشته باشی، کل پیش‌فرض جایگزین می‌شود (مثل اینکه به‌جای قهوه، یک چیز دیگر سفارش بدهی).

دستور نهایی = ENTRYPOINT + (آرگومان‌های docker run یا در صورت نبودن، CMD).

مثال ۱: فقط CMD، قابل‌تغییر

Section titled “مثال ۱: فقط CMD، قابل‌تغییر”
ex1/Dockerfile
FROM alpine
CMD ["echo", "پیش‌فرض: سلام"]
Terminal window
docker build -t lx-ce-cmd ./ex1 >/dev/null 2>&1
docker run --rm lx-ce-cmd
docker run --rm lx-ce-cmd echo "من جایگزین شدم"
docker run --rm lx-ce-cmd hostname
خروجی
پیش‌فرض: سلام
من جایگزین شدم
701e5fa88b77

بدون آرگومان، CMD اجرا می‌شود. هر چیزی بعد از اسم image در docker run کل CMD را جایگزین می‌کند. به همین دلیل می‌توانی hostname یا sh بدهی و به‌جای برنامه‌ی پیش‌فرض آن را اجرا کنی.

مثال ۲: فقط ENTRYPOINT، آرگومان اضافه می‌شود

Section titled “مثال ۲: فقط ENTRYPOINT، آرگومان اضافه می‌شود”
ex2/Dockerfile
FROM alpine
ENTRYPOINT ["echo", "ENTRYPOINT:"]
Terminal window
docker build -t lx-ce-ep ./ex2 >/dev/null 2>&1
docker run --rm lx-ce-ep
docker run --rm lx-ce-ep سلام دنیا
docker run --rm lx-ce-ep hostname
خروجی
ENTRYPOINT:
ENTRYPOINT: سلام دنیا
ENTRYPOINT: hostname

حالا آرگومان‌های docker run به انتهای ENTRYPOINT اضافه می‌شوند (جایگزین نمی‌شوند). حتی hostname هم فقط یک آرگومان برای echo شد و چاپ شد، نه اینکه اجرا شود. ENTRYPOINT «هویت» image است.

ex3/Dockerfile
FROM alpine
ENTRYPOINT ["ping", "-c", "2"]
CMD ["localhost"]
Terminal window
docker build -t lx-ce-combo ./ex3 >/dev/null 2>&1
docker run --rm lx-ce-combo | sed -n '1p;$p'
docker run --rm lx-ce-combo 127.0.0.1 | sed -n '1p;$p'
خروجی
PING localhost (::1): 56 data bytes
round-trip min/avg/max = 0.052/0.062/0.072 ms
PING 127.0.0.1 (127.0.0.1): 56 data bytes
round-trip min/avg/max = 0.160/0.278/0.396 ms

برنامه‌ی ثابت ping -c 2 است و CMD هدف پیش‌فرض (localhost). بدون آرگومان به localhost ping می‌زند؛ با آرگومان (127.0.0.1) فقط هدف عوض می‌شود. این همان الگوی ابزار قابل‌استفاده است.

⚡ بررسی سریع

در ENTRYPOINT ["ping","-c","2"] و CMD ["localhost"]، با docker run img 8.8.8.8 چه اجرا می‌شود؟

مثال ۴: --entrypoint برای دیباگ

Section titled “مثال ۴: --entrypoint برای دیباگ”
Terminal window
docker run --rm --entrypoint sh lx-ce-combo -c 'echo "داخل shell هستم"; which ping'
docker image inspect lx-ce-combo --format 'Entrypoint: {{.Config.Entrypoint}} | Cmd: {{.Config.Cmd}}'
خروجی
داخل shell هستم
/bin/ping
Entrypoint: [ping -c 2] | Cmd: [localhost]

وقتی ENTRYPOINT اجازه‌ی اجرای shell نمی‌دهد، docker run --entrypoint sh آن را جایگزین می‌کند تا داخل image را بازرسی کنی (اینجا -c ... آرگومان همان sh است). با docker inspect هر دو آرایه را می‌بینی.

مثال ۵: فرم exec در برابر shell، و سیگنال‌ها

Section titled “مثال ۵: فرم exec در برابر shell، و سیگنال‌ها”

دو شکل نوشتن وجود دارد:

  • exec: CMD ["app", "arg"] (آرایه‌ی JSON). برنامه مستقیم PID 1 است.
  • shell: CMD app arg. داکر آن را داخل /bin/sh -c "app arg" می‌پیچد و shell PID 1 می‌شود.

اهمیتش وقتی معلوم می‌شود که docker stop سیگنال TERM را می‌فرستد. یک برنامه‌ی کوچک می‌نویسیم که TERM را بگیرد و تمیز تمام شود:

sig/app.sh
#!/bin/sh
trap 'echo "گرفتم: TERM"; exit 0' TERM
echo "شروع شد"
while true; do sleep 1; done
sig/Dockerfile.exec
FROM alpine
COPY app.sh /app.sh
RUN chmod +x /app.sh
ENTRYPOINT ["/app.sh"]
sig/Dockerfile.shell
FROM alpine
COPY app.sh /app.sh
RUN chmod +x /app.sh
CMD /app.sh && echo finished
Terminal window
docker build -q -f sig/Dockerfile.exec -t lx-ce-sig-exec ./sig >/dev/null 2>&1
docker build -q -f sig/Dockerfile.shell -t lx-ce-sig-shell ./sig >/dev/null 2>&1
for m in exec shell; do
docker run -d --name lx-ce1 lx-ce-sig-$m >/dev/null; sleep 1
s=$(python3 -c 'import time;print(time.time())'); docker stop lx-ce1 >/dev/null; e=$(python3 -c 'import time;print(time.time())')
echo "$m: زمان stop ≈ $(python3 -c "print(round($e-$s))") ثانیه | کد خروج: $(docker inspect lx-ce1 --format '{{.State.ExitCode}}') | لاگ: $(docker logs lx-ce1 2>&1 | tail -1)"
docker rm lx-ce1 >/dev/null
done
خروجی
exec: زمان stop ≈ 1 ثانیه | کد خروج: 0 | لاگ: گرفتم: TERM
shell: زمان stop ≈ 4 ثانیه | کد خروج: 137 | لاگ: شروع شد

در فرم exec برنامه PID 1 است، TERM را گرفت، پیام را چاپ کرد و با کد 0 تمام شد (سریع). در فرم shell (با && که shell را در حافظه نگه می‌دارد) PID 1 خود /bin/sh است و TERM را به برنامه نمی‌رساند؛ داکر چند ثانیه صبر کرد و بعد با KILL کشت (کد 137) و برنامه فرصت تمیزکاری نداشت. برای سرویس‌هایی که باید تمیز خاموش شوند (بستن اتصال دیتابیس، ذخیره‌ی وضعیت) همیشه از فرم exec استفاده کن.

حالا ببین در فرم shell چه پروسه‌هایی هستند:

Terminal window
docker run -d --name lx-ce2 lx-ce-sig-shell >/dev/null; sleep 1
docker exec lx-ce2 ps -o pid,ppid,args
docker rm -f lx-ce2 >/dev/null
خروجی
PID PPID COMMAND
1 0 /bin/sh -c /app.sh && echo finished
7 1 {app.sh} /bin/sh /app.sh
9 7 sleep 1
10 0 ps -o pid,ppid,args

پروسه‌ی اول /bin/sh -c ... (PID 1) و فرزندش /app.sh است.

در لینوکس پروسه‌ی شماره‌ی ۱ (init) رفتار ویژه دارد: سیگنال‌هایی که برایشان handler ثبت نشده، نادیده گرفته می‌شوند (برای اینکه کسی تصادفی init را نکُشد). نتیجه: برنامه‌ای که PID 1 است باید خودش SIGTERM را مدیریت کند (مثل trap در مثال)، وگرنه docker stop تا پایان مهلت صبر می‌کند و بعد KILL می‌زند. و پروسه‌ی PID 1 باید فرزندان zombie را جمع کند؛ برای همین گزینه‌ی docker run --init یک init کوچک (tini) را PID 1 می‌کند.

تنظیم docker run img docker run img X
فقط CMD ["a"] a X (جایگزین)
فقط ENTRYPOINT ["e"] e e X
ENTRYPOINT ["e"] + CMD ["a"] e a e X
ENTRYPOINT ["e"] + --entrypoint Y Y (جایگزین) Y X
CMD ENTRYPOINT
نقش آرگومان/دستور پیش‌فرض برنامه‌ی ثابت image
با آرگومان docker run جایگزین می‌شود به آن اضافه می‌شود
تعداد مؤثر فقط آخری فقط آخری
عوض کردن آرگومان‌های docker run --entrypoint
فرم نمونه PID 1 سیگنال‌ها گسترش $VAR
exec CMD ["app","a"] خود app مستقیم به app خیر
shell CMD app a /bin/sh -c به app نمی‌رسد بله (shell انجام می‌دهد)

۱) فرم shell و خاموش شدن کند

Section titled “۱) فرم shell و خاموش شدن کند”

مثال ۵: stop چند ثانیه صبر کرد و با 137 تمام شد. راه‌حل: فرم exec یا exec app داخل اسکریپت.

Terminal window
mkdir -p m2
printf "FROM alpine\nCMD ['echo','hi']\n" > m2/Dockerfile
docker build -q -t lx-ce-quote ./m2 >/dev/null 2>&1
docker run --rm lx-ce-quote 2>&1 | head -1
خروجی
/bin/sh: [echo,hi]: not found

JSON فقط گیومه‌ی دوتایی می‌پذیرد. با گیومه‌ی تکی، داکر آن را به‌عنوان فرم shell تعبیر می‌کند و دستور بی‌معنی ([echo,hi]) اجرا می‌شود. راه‌حل: ["echo","hi"].

۳) انتظار گسترش متغیر در فرم exec

Section titled “۳) انتظار گسترش متغیر در فرم exec”
Terminal window
mkdir -p m3
printf 'FROM alpine\nCMD ["echo","$HOME"]\n' > m3/Dockerfile
docker build -q -t lx-ce-var ./m3 >/dev/null 2>&1
docker run --rm lx-ce-var
docker run --rm --entrypoint sh lx-ce-var -c 'echo $HOME'
خروجی
$HOME
/root

فرم exec از shell رد نمی‌شود، پس $HOME عیناً چاپ شد. راه‌حل: اگر گسترش متغیر لازم است، یک shell صریح بگذار: CMD ["sh","-c","echo $HOME"].

۴) override فقط CMD وقتی ENTRYPOINT هست

Section titled “۴) override فقط CMD وقتی ENTRYPOINT هست”

مثال ۲: docker run lx-ce-ep hostname دستور hostname را اجرا نکرد، چون به echo اضافه شد. راه‌حل: برای عوض کردن برنامه از --entrypoint استفاده کن.

✎ تمرینآسان

image ای بساز که با CMD ["echo","hi"] کار کند و با docker run img echo bye خروجی bye بدهد.

دیدن جواب
Terminal window
mkdir -p e1 && printf 'FROM alpine\nCMD ["echo","hi"]\n' > e1/Dockerfile
docker build -q -t lx-ce-cmd ./e1 >/dev/null 2>&1
docker run --rm lx-ce-cmd
docker run --rm lx-ce-cmd echo bye
خروجی
hi
bye
✎ تمرینمتوسط

تمرین اصلی: یک image ابزارمانند بساز که آرگومان بگیرد: ENTRYPOINT ["echo","سلام"] و CMD ["دنیا"]. بدون آرگومان و با آرگومان ایران اجرایش کن.

دیدن جواب
Terminal window
mkdir -p e2 && printf 'FROM alpine\nENTRYPOINT ["echo","سلام"]\nCMD ["دنیا"]\n' > e2/Dockerfile
docker build -q -t lx-ce-ep ./e2 >/dev/null 2>&1
docker run --rm lx-ce-ep
docker run --rm lx-ce-ep ایران
خروجی
سلام دنیا
سلام ایران
✎ تمرینسخت

با --entrypoint وارد image ای شو که ENTRYPOINT دارد و ثابت کن Entrypoint و Cmd را چطور می‌خوانی. بعد توضیح بده چرا برای یک سرویس که باید با docker stop تمیز خاموش شود فرم exec لازم است.

دیدن جواب
Terminal window
mkdir -p e3 && printf 'FROM alpine\nENTRYPOINT ["echo","entry"]\nCMD ["default"]\n' > e3/Dockerfile
docker build -q -t lx-ce-combo ./e3 >/dev/null 2>&1
docker run --rm --entrypoint sh lx-ce-combo -c 'echo "داخل shell"'
docker image inspect lx-ce-combo --format 'Entrypoint={{.Config.Entrypoint}} Cmd={{.Config.Cmd}}'
خروجی
داخل shell
Entrypoint=[echo entry] Cmd=[default]

در فرم shell، /bin/sh PID 1 است و سیگنال TERM را به برنامه نمی‌رساند؛ برنامه فرصت تمیز خاموش شدن ندارد و پس از مهلت با KILL کشته می‌شود. در فرم exec برنامه خودش PID 1 است و TERM را مستقیم می‌گیرد.

؟ آزمونک
  1. CMD و ENTRYPOINT چه فرقی دارند؟

  2. در فرم shell (CMD app arg) چه کسی PID 1 است؟

  3. چرا docker stop روی فرم shell ممکن است با کد 137 تمام شود؟

  4. برای اجرای shell داخل image ای که ENTRYPOINT دارد چه می‌نویسی؟

  5. در CMD ['echo','hi'] (با گیومه‌ی تکی) چه می‌شود؟

  • CMD: دستور/آرگومان پیش‌فرض (آرگومان‌های docker run جایگزینش می‌شوند). ENTRYPOINT: برنامه‌ی ثابت (آرگومان‌ها به آن اضافه می‌شوند).
  • الگوی ابزار: ENTRYPOINT ["برنامه"] + CMD ["آرگومان‌های پیش‌فرض"].
  • docker run --entrypoint X برنامه را عوض می‌کند (مثلاً sh برای دیباگ).
  • فرم exec (["a","b"]) را بنویس: برنامه PID 1 است و سیگنال‌ها را مستقیم می‌گیرد. فرم shell سیگنال را قورت می‌دهد و $VAR را گسترش می‌دهد.
  • گیومه‌ی دوتایی در JSON؛ docker run --init برای zombie ها.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
CMD ["app","arg"]دستور پیش‌فرض (فرم exec)
ENTRYPOINT ["app"]برنامه‌ی ثابت
docker run img X Yجایگزین CMD (یا اضافه به ENTRYPOINT)
docker run --entrypoint sh -it imgعوض کردن ENTRYPOINT
docker image inspect img --format "{{.Config.Entrypoint}} {{.Config.Cmd}}"دیدن هر دو
docker run --init imginit کوچک به‌عنوان PID 1
docker stop -t 30 Nمهلت بیشتر برای خاموش شدن تمیز
docker top Nپروسه‌های کانتینر (PID 1 کیست؟)