تفاوت command و entrypoint در Docker Compose
در Docker Compose، تنظیم entrypoint باعث نادیده گرفته شدن CMD در image میشود. با بررسی 4 ترکیب اصلی این دو دستور، یاد بگیرید چگونه آرگومانها را به درستی مدیریت کنید.
تفاوت command و entrypoint در Docker Compose، در یک قاعده
در Docker Compose، دستور entrypoint: برنامهای را که اجرا میشود تعیین میکند و command: آرگومانهایی را که به آن برنامه داده میشود مشخص مینماید. پردازش کانتینر، لیست entrypoint است که لیست command به انتهای آن اضافه شده است. تمام رفتارهای دیگر در این صفحه از همین یک جمله ناشی میشوند.
این دو کلید با دو دستورالعمل در Dockerfile نگاشت میشوند. دستور entrypoint: جایگزین ENTRYPOINT در image میشود. دستور command: جایگزین CMD در image میشود. این دو مستقل از هم نیستند و مشکل کاربران دقیقاً همینجاست: تنظیم entrypoint: باعث میشود CMD موجود در image نیز نادیده گرفته شود. مشخصات Compose مستقیماً به این موضوع اشاره دارد. اگر entrypoint مقدار غیر تهی داشته باشد، Compose هرگونه دستور پیشفرض موجود در image را نادیده میگیرد.
بررسی تنظیمات پیشفرض ایمیج
پیش از آنکه هر تغییری اعمال کنید، بررسی کنید که ایمیج بهصورت پیشفرض چه تنظیماتی را ارائه میدهد.
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: را جایگزین کنید، هیچکدام از مراحل راهاندازی اجرا نخواهند شد.
چهار ترکیب، نمایش داده شده در یک تصویر کوچک
تصویری بسازید که تنها وظیفه آن چاپ لیست آرگومانهایی است که با آن شروع شده است.
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از تصویر حذف شده است و هیچ هشداری دریافت نمیکنید. - هر دو کلید تنظیم شدهاند. پردازش
/bin/echo ep2 cmd2است. این تنها حالتی است که در آن کل لیست آرگومانها را کنترل میکنید.
چرا تنظیم entrypoint باعث پاک شدن CMD تصویر میشود
مقدار CMD در یک تصویر، به عنوان لیست آرگومانهای پیشفرض برای ENTRYPOINT آن تصویر نوشته میشود. وقتی entrypoint را جایگزین میکنید، آن آرگومانها متعلق به برنامهای میشوند که دیگر در حال اجرا نیست؛ بنابراین Compose آنها را حذف میکند تا از ساخت یک خط فرمان که نویسنده تصویر هرگز قصد ایجاد آن را نداشته، جلوگیری کند. docker run --entrypoint نیز به همین صورت عمل میکند، بنابراین این رفتار Docker است و نه یک ویژگی خاص در Compose.
نتیجه این موضوع کاملاً مشخص است. nginx:1.27 مقادیر ENTRYPOINT ["/docker-entrypoint.sh"] و CMD ["nginx", "-g", "daemon off;"] را اعلام میکند. اگر entrypoint: /custom-init.sh را تنظیم کنید، اسکریپت شما با یک لیست آرگومان خالی شروع میشود. اسکریپتی که به exec "$@" معمول ختم میشود، در این حالت چیزی برای اجرا (exec) ندارد؛ بنابراین exec کاری انجام نمیدهد، اسکریپت به آخرین خط خود میرسد و کانتینر با کد 0 و بدون هیچ پیام خطایی خارج میشود. آرگومانها را خودتان دوباره اضافه کنید:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]قانون کلی این است: هر زمان که entrypoint: را تنظیم میکنید، در همان ویرایش تصمیم بگیرید که command: چه باید باشد.
فرم اجرایی و فرم shell، و تفاوت آن در Compose
یک Dockerfile دو نوع نحو (syntax) را میپذیرد. CMD ["nginx", "-g", "daemon off;"] فرم اجرایی (exec form) است: باینری مستقیماً اجرا میشود و هیچ shellای درگیر نیست. CMD nginx -g "daemon off;" فرم shell است: Docker آن را به /bin/sh -c 'nginx -g "daemon off;"' بازنویسی میکند، بنابراین یک shell ابتدا اجرا شده و برنامه شما به فرزند آن تبدیل میشود.
Compose از این قاعده پیروی نمیکند و این موضوع برای کاربران غافلگیرکننده است. یک رشته در command: به آرگومانها تقسیم شده و مستقیماً اجرا میشود، بدون اینکه هیچ wrapperای از نوع /bin/sh -c وجود داشته باشد. مستندات Compose در این مورد صریح است: فیلد command در بستر SHELL که در image تعریف شده اجرا نمیشود، بنابراین اگر به قابلیتهای shell نیاز دارید، باید خودتان یک shell را فراخوانی کنید.
به همین دلیل است که command: echo "hello $$HOSTNAME" متن تحتاللفظی hello $HOSTNAME را چاپ میکند. هیچ shellای آن رشته را ندیده است، بنابراین هیچچیز گسترش (expand) نیافته است. هر زمان که به 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 وضعیت ویژهای دارد. هسته (kernel) عمل پیشفرض سیگنالها را روی PID 1 اعمال نمیکند؛ بنابراین فرآیندی که هیچ handler برای SIGTERM تعریف نکرده باشد، هنگام اجرا به عنوان PID 1، سیگنال SIGTERM را نادیده میگیرد. فرآیند تا پایان دوره انتظار باقی میماند و سپس به اجبار کشته میشود که این امر باعث قطع اتصالات باز یا تراکنشهای ثبتنشده میگردد.
وجود یک shell در ابتدای برنامه، احتمال بروز این مشکل را بیشتر میکند، زیرا shell به عنوان PID 1 عمل کرده و اکثر shellها سیگنالها را به فرزندان خود منتقل نمیکنند. برخی shellها خود را با دستور نهایی در یک رشته -c جایگزین میکنند، بنابراین گاهی برنامه شما همچنان به PID 1 میرسد. این موضوع به نوع shell و رشته دقیق دستور بستگی دارد، پس حدس نزنید. آن را بررسی کنید:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoاگر PID 1 به جای برنامه شما، /bin/sh -c ... را نشان میدهد، دو راه حل وجود دارد. از فرم exec در image استفاده کنید یا shell را حفظ کرده و فرآیند را با exec به برنامه بسپارید:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec به جای ایجاد یک فرآیند فرزند (fork)، فرآیند shell را با برنامه شما جایگزین میکند؛ در نتیجه برنامه شما PID 1 را به ارث برده و سیگنال را دریافت میکند.
برخی برنامهها فرزندانی ایجاد میکنند و هرگز آنها را جمعآوری (reap) نمیکنند که منجر به ایجاد فرآیندهای زامبی میشود، زیرا PID 1 مسئولیت جمعآوری فرآیندهای یتیم را نیز بر عهده دارد. Compose برای این مورد یک سوئیچ دارد:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true یک فرآیند init کوچک را به عنوان PID 1 اجرا میکند که سیگنالها را به برنامه شما منتقل کرده و فرزندان را جمعآوری میکند. stop_grace_period فضای بیشتری برای یک خاموشی واقعاً کند فراهم میکند. اگر برنامه شما انتظار سیگنال متفاوتی را دارد، stop_signal: SIGQUIT تعیین میکند که Compose چه سیگنالی ارسال کند. با استفاده از docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27 بررسی کنید که یک image چه درخواستی دارد.
اگر در یک stack، دستور 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 اجرا میشود تا || معنا پیدا کند. نوشتن healthcheck برای Compose که به درستی شکست میخورند بقیه جزئیات این بخش را پوشش میدهد.
افزودن یک flag به image رسمی
این همان بخشی است که اکثر خوانندگان به دنبال آن هستند. شما میخواهید یک flag اضافی به postgres اضافه کنید و نباید اسکریپت اولیه (initialization) را مختل کنید.
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 همچنان اجرا میشود و آنچه را که به آن دادهاید، اجرا (exec) میکند. به جای فرض کردن، نتیجه را بررسی کنید:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'خروجی باید 200 را نشان دهد. اگر همچنان 100 را نشان میدهد، docker compose config را اجرا کنید و تأیید کنید که command مورد انتظار شما در خروجی ادغامشده وجود دارد. Compose فایلهای override را با جایگزینی کامل command ادغام میکند، نه با الحاق به آن؛ بنابراین فایل دومی که command: را تنظیم میکند، بیسروصدا برنده میشود.
مقدار ${POSTGRES_PASSWORD} در بالا، توسط Compose روی host و از فایل .env شما، پیش از ایجاد container بسط داده میشود. فایلهای Env و secretها در Compose توضیح میدهد که این مقدار در کجا میتواند با امنیت نگهداری شود.
اجرای یک مهاجرت داده (migration) یکباره با 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) تنظیم نشده است. آن را در زمان build با COPY --chmod=0755 entrypoint.sh /entrypoint.sh تنظیم کنید.
خطایی که به no such file or directory ختم میشود برای فایلی که بهوضوح در تصویر میبینید. اسکریپت دارای پایانههای خط (line endings) ویندوزی است. در نتیجه، خط اول آن به صورت #!/bin/sh به همراه یک بایت carriage return خوانده میشود؛ بنابراین هسته سیستمعامل به دنبال مفسری با آن بایت در نامش میگردد و چیزی پیدا نمیکند. دستور dos2unix entrypoint.sh را اجرا کنید و سپس * text eol=lf را به .gitattributes اضافه کنید تا این مشکل تکرار نشود.
executable file not found in $PATH. فایل باینری که در command: نام برده شده در تصویر وجود ندارد، یا شما یک دستور داخلی shell مانند cd را در جایی نوشتهاید که فقط یک برنامه واقعی مجاز به قرارگیری است.
دسترسی به شل در ایمیجی که entrypoint آن با خطا مواجه میشود
هنگامی که entrypoint پیش از آنکه بتوانید چیزی را بررسی کنید متوقف میشود، آن را جایگزین کنید:
docker compose run --rm --entrypoint /bin/sh appاگر دستور بالا executable file not found in $PATH را برگرداند، ایمیج اصلاً شل ندارد. ایمیجهای Distroless و مبتنی بر scratch معمولاً شل ندارند. شما همچنان میتوانید بدون اجرای entrypoint، فایلسیستم را از بیرون بخوانید:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeهنگامی که نیاز دارید کانتینر روشن بماند تا بتوانید چندین بار به آن متصل شوید، آن را روی پردازشی که هرگز متوقف نمیشود پارک کنید. این مورد را در یک فایل override که commit نمیکنید قرار دهید:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []استفاده از command: [] ضرورتی ندارد، زیرا تنظیم entrypoint: قبلاً CMD ایمیج را پاک کرده است، اما نوشتن آن هدف شما را برای کسی که بعداً فایل را میخواند مشخص میکند. آن را بالا بیاورید و وارد شوید:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/shحالا entrypoint واقعی را بهصورت دستی اجرا کنید و ببینید کجا متوقف میشود. این کار پیام خطا را بهجای کانتینری که نیم ثانیه پیش از بین رفته، مستقیماً در ترمینال شما نمایش میدهد. اگر هنوز در حال اسمبل کردن اولین استک خود هستید، اولین استک Compose روی یک VPS ساختار فایلی که تمام موارد بالا بر اساس آن فرض شدهاند را پوشش میدهد.
FAQ
چرا کانتینر من بلافاصله پس از اجرای docker compose up متوقف میشود؟
کد خروج را در docker compose ps -a بررسی کنید. خروج با کد 0 بدون خروجی معمولاً به این معناست که شما entrypoint: را برای سرویس تنظیم کردهاید که باعث پاک شدن CMD تصویر (image) شده است؛ در نتیجه entrypoint با لیست آرگومانهای خالی اجرا شده و بلافاصله پایان یافته است. آرگومانها را با استفاده از command: دوباره اضافه کنید. خطایی که به permission denied ختم میشود به این معناست که اسکریپت entrypoint مجوز اجرا (executable bit) ندارد. خطایی که به no such file or directory ختم میشود برای فایلی که وجود دارد، نشاندهنده این است که اسکریپت دارای پایاندهندههای خط ویندوزی (Windows line endings) است و در نتیجه، خط shebang مفسری را فراخوانی میکند که در سیستم وجود ندارد.
آیا تنظیم entrypoint در Compose باعث حذف CMD تصویر میشود؟
بله. اگر entrypoint مقدار غیر تهی داشته باشد، Compose هر دستور پیشفرضی که در تصویر تعریف شده باشد را نادیده میگیرد. این رفتار مستند شده است و با docker run --entrypoint مطابقت دارد. دلیل این است که CMD یک تصویر به عنوان آرگومان برای ENTRYPOINT همان تصویر نوشته شده است؛ بنابراین به محض جایگزینی entrypoint، آرگومانهای قدیمی دیگر به چیزی تعلق ندارند. اگر entrypoint جدید همچنان به آرگومان نیاز دارد، command: را در همان سرویس تنظیم کنید.
آیا رشتهای که در دستور Compose قرار دارد توسط shell اجرا میشود؟
خیر. برخلاف CMD در Dockerfile، یک رشته در command: در Compose به آرگومانها تقسیم شده و مستقیماً اجرا میشود، بدون اینکه هیچ wrapper از نوع /bin/sh -c داشته باشد. بنابراین $VARIABLE هرگز توسط shell داخل کانتینر بسط داده نمیشود. هر زمان به shell نیاز داشتید، خودتان آن را فراخوانی کنید، مانند command: /bin/sh -c 'echo "hello $$HOSTNAME"'. استفاده از $$ مضاعف، علامت دلار را escape میکند تا Compose آن را به جای بسط دادن در میزبان (host)، مستقیماً به کانتینر منتقل کند.
چرا دستور docker compose down برای یک کانتینر ده ثانیه طول میکشد؟
Compose سیگنال SIGTERM را به PID 1 ارسال میکند، به مدت stop_grace_period (بهطور پیشفرض 10 ثانیه) منتظر میماند و سپس SIGKILL را میفرستد. هسته سیستمعامل اقدامات پیشفرض سیگنال را برای PID 1 اعمال نمیکند، بنابراین برنامهای که هیچ handler برای SIGTERM ندارد، سیگنال را نادیده میگیرد و همیشه کل دوره انتظار را سپری میکند. با استفاده از docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' ' بررسی کنید که PID 1 واقعاً چیست. اگر یک shell است، تصویر را به فرم exec تغییر دهید یا در رشته shell از exec استفاده کنید. اگر فرآیند فرزندانی ایجاد میکند که هرگز آنها را جمعآوری (reap) نمیکند، init: true را روی سرویس تنظیم کنید.