توی این درس یاد میگیری وقتی 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 داشته باشی، کل پیشفرض جایگزین میشود (مثل اینکه بهجای قهوه، یک چیز دیگر سفارش بدهی).
چه چیزی اجرا میشود؟
Section titled “چه چیزی اجرا میشود؟”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: فقط CMD، قابلتغییر
Section titled “مثال ۱: فقط CMD، قابلتغییر”FROM alpineCMD ["echo", "پیشفرض: سلام"]docker build -t lx-ce-cmd ./ex1 >/dev/null 2>&1docker run --rm lx-ce-cmddocker 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، آرگومان اضافه میشود”FROM alpineENTRYPOINT ["echo", "ENTRYPOINT:"]docker build -t lx-ce-ep ./ex2 >/dev/null 2>&1docker run --rm lx-ce-epdocker run --rm lx-ce-ep سلام دنیاdocker run --rm lx-ce-ep hostnameENTRYPOINT:ENTRYPOINT: سلام دنیاENTRYPOINT: hostnameحالا آرگومانهای docker run به انتهای ENTRYPOINT اضافه میشوند (جایگزین نمیشوند). حتی hostname هم فقط یک آرگومان برای echo شد و چاپ شد، نه اینکه اجرا شود. ENTRYPOINT «هویت» image است.
مثال ۳: ترکیب: ENTRYPOINT + CMD
Section titled “مثال ۳: ترکیب: ENTRYPOINT + CMD”FROM alpineENTRYPOINT ["ping", "-c", "2"]CMD ["localhost"]docker build -t lx-ce-combo ./ex3 >/dev/null 2>&1docker 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 bytesround-trip min/avg/max = 0.052/0.062/0.072 msPING 127.0.0.1 (127.0.0.1): 56 data bytesround-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 چه اجرا میشود؟
آرگومان docker run جایگزین CMD میشود و به ENTRYPOINT اضافه میگردد.
مثال ۴: --entrypoint برای دیباگ
Section titled “مثال ۴: --entrypoint برای دیباگ”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/pingEntrypoint: [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 را بگیرد و تمیز تمام شود:
#!/bin/shtrap 'echo "گرفتم: TERM"; exit 0' TERMecho "شروع شد"while true; do sleep 1; doneFROM alpineCOPY app.sh /app.shRUN chmod +x /app.shENTRYPOINT ["/app.sh"]FROM alpineCOPY app.sh /app.shRUN chmod +x /app.shCMD /app.sh && echo finisheddocker build -q -f sig/Dockerfile.exec -t lx-ce-sig-exec ./sig >/dev/null 2>&1docker build -q -f sig/Dockerfile.shell -t lx-ce-sig-shell ./sig >/dev/null 2>&1for 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/nulldoneexec: زمان stop ≈ 1 ثانیه | کد خروج: 0 | لاگ: گرفتم: TERMshell: زمان stop ≈ 4 ثانیه | کد خروج: 137 | لاگ: شروع شددر فرم exec برنامه PID 1 است، TERM را گرفت، پیام را چاپ کرد و با کد 0 تمام شد (سریع). در فرم shell (با && که shell را در حافظه نگه میدارد) PID 1 خود /bin/sh است و TERM را به برنامه نمیرساند؛ داکر چند ثانیه صبر کرد و بعد با KILL کشت (کد 137) و برنامه فرصت تمیزکاری نداشت. برای سرویسهایی که باید تمیز خاموش شوند (بستن اتصال دیتابیس، ذخیرهی وضعیت) همیشه از فرم exec استفاده کن.
حالا ببین در فرم shell چه پروسههایی هستند:
docker run -d --name lx-ce2 lx-ce-sig-shell >/dev/null; sleep 1docker exec lx-ce2 ps -o pid,ppid,argsdocker rm -f lx-ce2 >/dev/nullPID 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 است.
پشت پرده
Section titled “پشت پرده”PID 1 چه خاصیتی دارد؟
Section titled “PID 1 چه خاصیتی دارد؟”در لینوکس پروسهی شمارهی ۱ (init) رفتار ویژه دارد: سیگنالهایی که برایشان handler ثبت نشده، نادیده گرفته میشوند (برای اینکه کسی تصادفی init را نکُشد). نتیجه: برنامهای که PID 1 است باید خودش SIGTERM را مدیریت کند (مثل trap در مثال)، وگرنه docker stop تا پایان مهلت صبر میکند و بعد KILL میزند. و پروسهی PID 1 باید فرزندان zombie را جمع کند؛ برای همین گزینهی docker run --init یک init کوچک (tini) را PID 1 میکند.
قانون ترکیب
Section titled “قانون ترکیب”| تنظیم | 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 |
جدولهای مرجع
Section titled “جدولهای مرجع”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 انجام میدهد) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فرم shell و خاموش شدن کند
Section titled “۱) فرم shell و خاموش شدن کند”مثال ۵: stop چند ثانیه صبر کرد و با 137 تمام شد. راهحل: فرم exec یا exec app داخل اسکریپت.
۲) گیومهی تکی در JSON
Section titled “۲) گیومهی تکی در JSON”mkdir -p m2printf "FROM alpine\nCMD ['echo','hi']\n" > m2/Dockerfiledocker build -q -t lx-ce-quote ./m2 >/dev/null 2>&1docker run --rm lx-ce-quote 2>&1 | head -1/bin/sh: [echo,hi]: not foundJSON فقط گیومهی دوتایی میپذیرد. با گیومهی تکی، داکر آن را بهعنوان فرم shell تعبیر میکند و دستور بیمعنی ([echo,hi]) اجرا میشود. راهحل: ["echo","hi"].
۳) انتظار گسترش متغیر در فرم exec
Section titled “۳) انتظار گسترش متغیر در فرم exec”mkdir -p m3printf 'FROM alpine\nCMD ["echo","$HOME"]\n' > m3/Dockerfiledocker build -q -t lx-ce-var ./m3 >/dev/null 2>&1docker run --rm lx-ce-vardocker 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 بدهد.
دیدن جواب
mkdir -p e1 && printf 'FROM alpine\nCMD ["echo","hi"]\n' > e1/Dockerfiledocker build -q -t lx-ce-cmd ./e1 >/dev/null 2>&1docker run --rm lx-ce-cmddocker run --rm lx-ce-cmd echo byehibyeتمرین اصلی: یک image ابزارمانند بساز که آرگومان بگیرد: ENTRYPOINT ["echo","سلام"] و CMD ["دنیا"]. بدون آرگومان و با آرگومان ایران اجرایش کن.
دیدن جواب
mkdir -p e2 && printf 'FROM alpine\nENTRYPOINT ["echo","سلام"]\nCMD ["دنیا"]\n' > e2/Dockerfiledocker build -q -t lx-ce-ep ./e2 >/dev/null 2>&1docker run --rm lx-ce-epdocker run --rm lx-ce-ep ایرانسلام دنیاسلام ایرانبا --entrypoint وارد image ای شو که ENTRYPOINT دارد و ثابت کن Entrypoint و Cmd را چطور میخوانی. بعد توضیح بده چرا برای یک سرویس که باید با docker stop تمیز خاموش شود فرم exec لازم است.
دیدن جواب
mkdir -p e3 && printf 'FROM alpine\nENTRYPOINT ["echo","entry"]\nCMD ["default"]\n' > e3/Dockerfiledocker build -q -t lx-ce-combo ./e3 >/dev/null 2>&1docker run --rm --entrypoint sh lx-ce-combo -c 'echo "داخل shell"'docker image inspect lx-ce-combo --format 'Entrypoint={{.Config.Entrypoint}} Cmd={{.Config.Cmd}}'داخل shellEntrypoint=[echo entry] Cmd=[default]در فرم shell، /bin/sh PID 1 است و سیگنال TERM را به برنامه نمیرساند؛ برنامه فرصت تمیز خاموش شدن ندارد و پس از مهلت با KILL کشته میشود. در فرم exec برنامه خودش PID 1 است و TERM را مستقیم میگیرد.
آزمونک
Section titled “آزمونک”CMD و ENTRYPOINT چه فرقی دارند؟
ترکیبشان الگوی ابزار قابلاستفاده است.
در فرم shell (CMD app arg) چه کسی PID 1 است؟
shell سیگنال TERM را به app نمیرساند.
چرا docker stop روی فرم shell ممکن است با کد 137 تمام شود؟
137 = 128 + 9 (SIGKILL).
برای اجرای shell داخل image ای که ENTRYPOINT دارد چه مینویسی؟
آرگومانها فقط به ENTRYPOINT اضافه میشوند؛ --entrypoint آن را عوض میکند.
در CMD ['echo','hi'] (با گیومهی تکی) چه میشود؟
JSON فقط گیومهی دوتایی دارد.
جمعبندی
Section titled “جمعبندی”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 img | init کوچک بهعنوان PID 1 |
docker stop -t 30 N | مهلت بیشتر برای خاموش شدن تمیز |
docker top N | پروسههای کانتینر (PID 1 کیست؟) |