SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor

نحوه دسترسی به شل تعاملی در Docker Compose

برای ورود به کانتینر در حال اجرا از دستور docker compose exec استفاده کنید. اگر سرویس متوقف شده است یا قصد ایجاد کانتینر جدید دارید از docker compose run --rm بهره ببرید.

دسترسی به شل تعاملی با docker compose exec

docker compose exec web bash یک شل تعاملی را درون کانتینری که هم‌اکنون با سرویس web در حال اجراست، باز می‌کند. نامی که پس از exec می‌آید، نام سرویس در فایل compose.yaml شماست، نه نام کانتینر. اگر ایمیج فاقد bash است، از sh استفاده کنید.

docker compose ps
docker compose exec web bash

ابتدا docker compose ps را اجرا کنید. این دستور باید web را با وضعیت running فهرست کند. سپس دستور دوم شما را به یک پرامپت درون کانتینر می‌برد و exit یا Ctrl-D شما را به میزبان بازمی‌گرداند. سرویس پس از خروج شما همچنان به کار خود ادامه می‌دهد، زیرا exec یک پردازش دوم در کنار پردازش اصلی ایجاد کرده است. بستن شل، پردازش PID 1 (شناسه پردازش 1) که کانتینر برای اجرای آن ساخته شده است را تحت تأثیر قرار نمی‌دهد.

این یکی از دو روش ورود به کانتینر است. دستور exec به کانتینری که از قبل وجود دارد متصل می‌شود. دستور docker compose run یک کانتینر جدید از همان تعریف سرویس ایجاد می‌کند. تقریباً تمام موارد دیگر در این راهنما بر اساس همین تفاوت ساده است.

چرا -it در Compose اختیاری است اما در docker معمولی الزامی است

دو فلگ، تعاملی بودن یک نشست را کنترل می‌کنند. -i ورودی استاندارد (stdin) را باز نگه می‌دارد تا آنچه تایپ می‌کنید به پردازش برسد. -t یک ترمینال مجازی موسوم به TTY اختصاص می‌دهد تا shell بتواند prompt را چاپ کرده و کلیدهای جهت‌نما را مدیریت کند. دستور docker exec به‌صورت پیش‌فرض هر دو را غیرفعال نگه می‌دارد؛ به همین دلیل است که در تمام مثال‌هایی که دیده‌اید، از docker exec -it استفاده شده است. docker compose exec هر دو را برای شما فعال می‌کند، بنابراین docker compose exec -it web bash و docker compose exec web bash عملکرد یکسانی دارند. Compose همچنان -it را می‌پذیرد تا عادت‌های قدیمی شما همچنان کار کنند.

شما نبود TTY را در عرض چند ثانیه متوجه می‌شوید. shell اجرا می‌شود، اما هیچ promptای چاپ نمی‌کند و Ctrl-C هرگز به پردازش نمی‌رسد. در حالت معکوس، یعنی زمانی که باید از Compose بخواهید TTY اختصاص ندهد، فلگ مخصوص به خود و بخش جداگانه‌ای در ادامه وجود دارد.

چه زمانی که image فاقد bash است چه باید کرد

اگر از یک image مبتنی بر Alpine درخواست bash کنید و دستور exec با خطای زیر مواجه شود:

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

این پیام یک مشکل در اجرای دستور (exec) نیست. این پیام بیانگر آن است که باینری مورد نظر شما در image وجود ندارد. Alpine از BusyBox استفاده می‌کند که ash را به عنوان /bin/sh ارائه می‌دهد و اصلاً bash ندارد؛ بنابراین از sh استفاده کنید:

docker compose exec web sh

imageهای مبتنی بر Debian و Ubuntu، از جمله تگ‌های -slim، دارای bash هستند و bash تاریخچه دستورات و تکمیل خودکار بهتری را در اختیار شما قرار می‌دهد. بنابراین ابتدا bash را امتحان کنید و در صورت عدم وجود، به سراغ sh بروید. sh تقریباً در تمام imageهای عمومی وجود دارد.

برخی imageها اصلاً shell ندارند. imageهای Distroless و imageهایی که به صورت FROM scratch ساخته شده‌اند، فقط شامل باینری برنامه و کتابخانه‌های آن هستند و هیچ چیز دیگری ندارند؛ این کار عمدی است، زیرا shellای که وجود ندارد، نمی‌تواند علیه شما استفاده شود. در این موارد، sh با همان پیام خطا مواجه می‌شود و دیگر چیزی برای امتحان کردن باقی نمی‌ماند. دو رویکرد در اینجا کارساز است. imageهای distroless گوگل، تگ‌های :debug را منتشر می‌کنند که یک shell از نوع BusyBox اضافه می‌کنند، بنابراین تغییر موقت تگ به شما اجازه ورود می‌دهد. یا یک container جداگانه در داخل namespaceهای هدف اجرا کنید:

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

اکنون ابزارهای netshoot به شبکه برنامه متصل شده‌اند، بنابراین curl localhost:8080 و ss -lntp طوری رفتار می‌کنند که گویی در داخل آن هستید. سیستم فایلی که می‌بینید متعلق به netshoot است، نه برنامه. از آنجا که namespace پردازش به اشتراک گذاشته شده است، ls /proc/1/root/ در صورتی که root باشید، به فایل‌های خودِ هدف دسترسی پیدا می‌کند.

هنگامی که سرویس در حال اجرا نیست، از docker compose run --rm استفاده کنید

دستور exec به یک کانتینر در حال اجرا نیاز دارد. اگر آن را به سمت یک سرویس متوقف‌شده هدایت کنید، با خطا مواجه می‌شوید:

service "web" is not running

این دستور هیچ چیزی را برای شما اجرا نمی‌کند. اما docker compose run این کار را انجام می‌دهد:

docker compose run --rm web bash

دستور run یک کانتینر جدید بر اساس تعریف سرویس web ایجاد می‌کند که دارای همان image، متغیرهای محیطی، volumeها و شبکه است و دستور اصلی سرویس را با دستوری که شما تایپ کرده‌اید جایگزین می‌کند. --rm پس از خروج شما، آن کانتینر را حذف می‌کند. اگر --rm را حذف کنید، کانتینرهای باقی‌مانده با نام‌هایی مانند myproject-web-run-4f1c2b جمع می‌شوند که docker compose ps -a آن‌ها را به شما نشان می‌دهد و هیچ فرآیند دیگری آن‌ها را پاکسازی نمی‌کند.

دو رفتار دستور run معمولاً کاربران را غافلگیر می‌کند. این دستور پورت‌های سرویس را منتشر نمی‌کند مگر اینکه --service-ports را اضافه کنید، و این یک تصمیم عمدی است: تلاش برای اتصال یک کانتینر دوم به پورت 8080 میزبان در حالی که کانتینر اول هنوز آن را اشغال کرده است، با خطای bind: address already in use مواجه می‌شود. همچنین این دستور پیش از ظاهر شدن shell شما، تمام مواردی که سرویس در بخش depends_on لیست کرده است را اجرا می‌کند؛ بنابراین یک بررسی سریع داخل کانتینر می‌تواند منجر به اجرای ناخواسته دیتابیس و کش شود. --no-deps از این مرحله صرف‌نظر می‌کند.

دستور run از طریق ENTRYPOINT موجود در image اجرا می‌شود، اما exec این‌طور نیست. دستور exec فرمان شما را مستقیماً در کانتینر موجود اجرا می‌کند، بنابراین اسکریپت entrypoint هرگز آن را نمی‌بیند. در حالت run، دستور bash شما به عنوان آرگومان به آن اسکریپت ارسال می‌شود. بسیاری از imageهای رسمی، entrypoint خود را با exec "$@" به پایان می‌رسانند تا دستور مستقیماً عبور کند و شما به shell دسترسی پیدا کنید. اسکریپتی که آرگومان‌های خود را تفسیر می‌کند، رفتار متفاوتی خواهد داشت؛ در این صورت باید entrypoint را برای همان یک بار اجرا جایگزین کنید:

docker compose run --rm --entrypoint sh web

این رایج‌ترین دلیل تفاوت رفتار دستوری است که در exec کار می‌کند اما در run متفاوت عمل می‌کند، و تفاوت بین command و entrypoint توضیح می‌دهد که در هر بار اجرا، کدام بخش از پیکربندی image را جایگزین می‌کنید.

انتخاب بین exec یا run

  • دستور exec به یک کانتینر در حال اجرا نیاز دارد. دستور run نیازی به کانتینر در حال اجرا ندارد و می‌تواند وابستگی‌ها را نیز راه‌اندازی کند.
  • دستور exec لیست پردازش‌های زنده و فایل‌ها را دقیقاً همان‌طور که در لحظه هستند نشان می‌دهد، از جمله هر تغییری که برنامه از زمان شروع اجرا ایجاد کرده است. دستور run یک کپی تمیز از image را دریافت می‌کند، بنابراین هیچ‌کدام از تغییرات در آن وجود ندارد.
  • دستور exec از entrypoint عبور می‌کند (آن را اجرا نمی‌کند). دستور run آن را اجرا می‌کند.
  • دستور run یک کانتینر باقی‌مانده ایجاد می‌کند، مگر اینکه از فلگ --rm استفاده کنید.

از exec برای بررسی آنچه در حال حاضر رخ می‌دهد استفاده کنید. از run --rm برای ایجاد یک کپی موقت از همان محیط، برای اجرای یک دستور migration تکی، یا زمانی که سرویس اصلی به اندازه کافی برای ورود با exec پایدار نمی‌ماند، استفاده کنید.

فلگ‌های کاربردی exec: کاربر، دایرکتوری کاری و نسخه‌ها (replicas)

بیشتر ایمیج‌ها با کاربری غیر از root اجرا می‌شوند، بنابراین نصب ابزارهای عیب‌یابی در shell مربوط به exec با محدودیت مواجه می‌شود:

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u root یک shell با دسترسی root در همان container به شما می‌دهد:

docker compose exec -u root web sh

-w /srv/app دایرکتوری کاری را فقط برای همان دستور تنظیم می‌کند. -e KEY=value یک متغیر محیطی (environment variable) به session شما اضافه می‌کند که روی سرویس اصلی تأثیری ندارد. هنگامی که یک سرویس بیش از یک replica دارد، --index 2 تعیین می‌کند که به کدام container وارد شوید. اگر به دنبال بررسی مالکیت فایل‌ها در یک دایرکتوری mount شده هستید، PUID و PGID در ایمیج‌های container توضیح می‌دهد که چرا شناسه‌های عددی (numeric ids) و نه نام کاربری، تعیین‌کننده دسترسی نوشتن در آن مسیر هستند.

دسترسی به shell مربوط به psql یا mysql در داخل container پایگاه‌داده

کلاینت در حال حاضر داخل image پایگاه‌داده قرار دارد، بنابراین نیازی به نصب آن روی host ندارید و لازم نیست پورت را publish کنید:

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

ایمیج‌های Postgres شامل psql، ایمیج‌های MySQL شامل mysql و ایمیج‌های MariaDB شامل mariadb هستند. اتصال از داخل container برقرار می‌شود، بنابراین این روش حتی زمانی که فایل compose هیچ پورتی را برای پایگاه‌داده publish نکرده باشد، کار می‌کند. این امن‌ترین حالت است: هیچ چیزی در اینترنت نمی‌تواند به پورتی که هرگز publish نکرده‌اید، دسترسی پیدا کند.

یک تله وجود دارد که وقت زیادی از کاربران می‌گیرد. shell شما متغیرها را روی host و پیش از آنکه Docker دستور را ببیند، expand می‌کند؛ بنابراین -U "$POSTGRES_USER" یک رشته خالی ارسال می‌کند، زیرا آن متغیر فقط داخل container وجود دارد. استفاده از کوتیشن‌های تکی و یک shell داخل container باعث می‌شود expand در جای درست انجام شود:

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

در اینجا از docker compose run --rm db بدون هیچ دستوری استفاده نکنید. این کار یک سرور Postgres دوم را روی همان volume داده‌ها اجرا می‌کند و سرور از شروع به کار امتناع خواهد کرد:

FATAL:  lock file "postmaster.pid" already exists

فایل lock وظیفه خود را به درستی انجام می‌دهد، زیرا اگر دو سرور همزمان در یک دایرکتوری داده بنویسند، باعث خرابی داده‌ها (corrupt) می‌شود. تا زمانی که پایگاه‌داده بالا است، با استفاده از exec وارد container در حال اجرا شوید. اینکه آیا پایگاه‌داده اصلاً باید در Compose باشد یا خیر، یک تصمیم جداگانه است و اجرای پایگاه‌داده در Docker یا روی host مزایا و معایب آن را بررسی می‌کند.

سرویس‌هایی که در زمان راه‌اندازی به کنسول نیاز دارند: stdin_open و tty

دستورات exec و run پوسته‌هایی را پوشش می‌دهند که به‌صورت دستی باز می‌کنید. سرویسی که پردازش اصلی آن ذاتاً تعاملی است، به دو کلید در فایل compose نیاز دارد:

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: true همان docker run -i است و tty: true همان docker run -t است. بدون این دو، کانتینر بلافاصله با کد 0 شروع شده و خارج می‌شود و docker compose ps -a مقدار Exited (0) را نشان می‌دهد. هیچ‌چیز کرش نکرده است. python بدون ترمینال روی stdin، بلافاصله به انتهای فایل (EOF) می‌رسد و به‌طور عادی خارج می‌شود؛ این رفتار صحیح برای برنامه‌ای است که کسی در حال تایپ کردن در آن نیست.

با تنظیم هر دو کلید، به پردازش در حال اجرا متصل شوید:

docker attach $(docker compose ps -q console)

با استفاده از Ctrl-P و سپس Ctrl-Q از کانتینر جدا شوید؛ این کار باعث می‌شود پردازش همچنان در حال اجرا باقی بماند. این توالی تنها زمانی کار می‌کند که کانتینر هم TTY داشته باشد و هم stdin آن باز باشد. در مقابل، Ctrl-C یک سیگنال وقفه (interrupt) به PID 1 ارسال کرده و سرویس را متوقف می‌کند.

برای سرویس‌های معمولی، هر دو کلید را غیرفعال بگذارید. یک وب‌سرور هرگز stdin را نمی‌خواند و tty: true باعث می‌شود بسیاری از برنامه‌ها به خروجی رنگی و line buffering سوئیچ کنند، زیرا تصور می‌کنند یک انسان در حال مشاهده خروجی است؛ این کار باعث پر شدن docker compose logs با کدهای escape می‌شود.

چرا اجرای اسکریپتی در cron و CI با خطا مواجه می‌شود: پرچم -T

دستور exec که در ترمینال شما به‌درستی کار می‌کند، در یک job مربوط به cron یا یک runner در CI (یکپارچه‌سازی مداوم) با خطا مواجه می‌شود:

the input device is not a TTY

ابزار Compose به‌صورت پیش‌فرض درخواست یک pseudo terminal می‌کند، اما cron هیچ ترمینالی به job اختصاص نمی‌دهد؛ بنابراین درخواست پیش از اجرای دستور شما شکست می‌خورد. استفاده از -T این درخواست را غیرفعال می‌کند:

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

استفاده از -T به دلیل دومی نیز اهمیت دارد. یک TTY جریان بایت‌ها را در مسیر خروجی بازنویسی می‌کند، بنابراین یک dump فشرده که از آن عبور کند، آسیب‌دیده به مقصد می‌رسد. هر خروجی که redirect یا pipe شده باشد، به -T نیاز دارد.

دو نکته دیگر درباره cron: پرچم -f را با یک مسیر مطلق (absolute path) ارسال کنید، زیرا cron کار را از دایرکتوری home اجرا می‌کند که در آن فایل compose وجود ندارد و در نتیجه Compose با خطای no configuration file provided: not found متوقف می‌شود. همچنین، exec کد خروج (exit code) دستوری که اجرا کرده را بازمی‌گرداند؛ بنابراین یک pg_dump ناموفق، اسکریپت شما را تحت set -e با خطا مواجه می‌کند، به‌جای آنکه یک نسخه پشتیبان خالی بنویسد و وضعیت را موفق گزارش دهد. سایر دستورات روزمره در برگه تقلب دستورات Compose جمع‌آوری شده‌اند که نگهداری آن در کنار اسکریپت‌ها توصیه می‌شود.

چرا تغییراتی که داخل کانتینر اعمال می‌کنید ناپدید می‌شوند

شما ابزاری را با exec نصب می‌کنید، یک فایل پیکربندی را ویرایش می‌کنید و مشکل را برطرف می‌کنید، اما یک هفته بعد آن اصلاح از بین رفته است. این رفتار لایه قابل‌نوشتن (writable layer) کانتینر است که دقیقاً طبق طراحی عمل می‌کند. دستور docker compose up -d پس از هر تغییری در تگ image یا تعریف سرویس، کانتینر قدیمی را حذف کرده و یک کانتینر جدید از روی image می‌سازد؛ در نتیجه تمام ویرایش‌های دستی همراه با کانتینر قدیمی از بین می‌روند.

دستور docker compose restart متفاوت عمل می‌کند. این دستور همان کانتینر قبلی را متوقف و دوباره اجرا می‌کند، بنابراین ویرایش‌های دستی در آن باقی می‌مانند. به همین دلیل است که یک اصلاح دستی ممکن است هفته‌ها پایدار بماند و سپس در جریان یک به‌روزرسانی نامرتبط ناپدید شود. Volumeهای نام‌گذاری‌شده و bind mountها هر دو عملیات را تاب می‌آورند، زیرا داده‌های آن‌ها خارج از کانتینر ذخیره می‌شود و بخش bind mounts and named volumes توضیح می‌دهد که برای داده‌هایی که قصد حفظ آن‌ها را دارید، کدام‌یک را انتخاب کنید.

بنابراین، از shell در exec فقط برای خواندن و تست کردن استفاده کنید. هنگامی که از درستی اصلاح مطمئن شدید، آن را در جایی اعمال کنید که ماندگار باشد: نصب پکیج در Dockerfile و تنظیمات در فایل compose. سپس از docker compose up -d برای اعمال تغییرات استفاده کنید و با یک exec دیگر تأیید کنید که کانتینر جدید واقعاً آن تغییرات را دارد.

FAQ

تفاوت بین docker compose exec و docker compose run چیست؟

دستور exec یک فرمان را در کانتینری که از قبل در حال اجراست، در کنار پردازش اصلی اجرا می‌کند و entrypoint تصویر را نادیده می‌گیرد. دستور run یک کانتینر جدید از همان تعریف سرویس، با همان تصویر، متغیرهای محیطی، volumeها و شبکه‌ها ایجاد می‌کند، فرمان شما را از طریق entrypoint عبور می‌دهد و ابتدا تمام سرویس‌های depends_on را بالا می‌آورد. همچنین دستور run پورت‌های سرویس را منتشر نمی‌کند مگر اینکه --service-ports را اضافه کنید. از exec برای بررسی سرویس در حال اجرا استفاده کنید. از run --rm زمانی استفاده کنید که سرویس متوقف شده است یا نمی‌خواهید در عملکرد آن اختلال ایجاد کنید.

چرا docker compose exec گزارش می‌دهد که سرویس در حال اجرا نیست؟

دستور exec به یک کانتینر موجود متصل می‌شود و نمی‌تواند کانتینر جدیدی بسازد، بنابراین سرویس متوقف‌شده یا کرش‌کرده خطای service "web" is not running می‌دهد. وضعیت docker compose ps -a را بررسی کنید که کانتینرهای خارج‌شده (exited) با وضعیتی مانند Exited (1) را فهرست می‌کند و برای یافتن دلیل توقف، docker compose logs web را بخوانید. برای دسترسی به shell به هر طریقی، دستور docker compose run --rm --entrypoint sh web را اجرا کنید. این کار یک کانتینر تازه از همان تعریف سرویس می‌سازد بدون اینکه اجازه دهد فرمان شروعِ معیوب اجرا شود.

چگونه وقتی تصویر bash ندارد، یک shell باز کنم؟

شکست خوردن docker compose exec web bash با خطای exec: "bash": executable file not found in $PATH به این معنی است که bash در تصویر وجود ندارد، که برای هر چیزی که بر پایه Alpine ساخته شده، طبیعی است. از docker compose exec web sh استفاده کنید، زیرا BusyBox ابزار /bin/sh را فراهم می‌کند. تصاویر Distroless و scratch اصلاً shell ندارند، بنابراین هیچ دستور exec کار نخواهد کرد. اگر ناشر تصویر تگ :debug ارائه می‌دهد از آن استفاده کنید، یا یک کانتینر دیباگ در namespaceهای هدف با docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot راه‌اندازی کنید، که در آن $CID از docker compose ps -q web به دست می‌آید.

چرا دستور exec من در cron با خطای "the input device is not a TTY" شکست می‌خورد؟

دستور docker compose exec به‌طور پیش‌فرض درخواست یک pseudo terminal می‌کند و cron هیچ ترمینالی ارائه نمی‌دهد، بنابراین درخواست پیش از اجرای فرمان شما شکست می‌خورد. برای غیرفعال کردن آن، -T را اضافه کنید: docker compose exec -T db pg_dump -U postgres app. برای هر خروجی که redirect یا pipe شده نیز از -T استفاده کنید، زیرا TTY جریان بایت‌ها را تغییر می‌دهد و به dumpهای باینری آسیب می‌زند. در cron، همچنین -f را با مسیر مطلق فایل compose خود ارسال کنید، در غیر این صورت Compose با خطای no configuration file provided: not found خارج می‌شود.

آیا تغییراتی که با exec داخل کانتینر ایجاد می‌کنم پس از restart باقی می‌مانند؟

این تغییرات پس از docker compose restart که از همان کانتینر استفاده مجدد می‌کند، باقی می‌مانند. اما پس از docker compose up -d و هرگونه تغییر در تصویر یا پیکربندی از بین می‌روند، زیرا کانتینر از نو بر اساس تصویر ساخته شده و لایه قابل‌نوشتن (writable layer) قبلی دور ریخته می‌شود. داده‌هایی که در volumeهای نام‌گذاری‌شده یا bind mountها نوشته شده‌اند، در هر دو حالت باقی می‌مانند، زیرا خارج از کانتینر قرار دارند. تغییرات تشخیصی را با exec انجام دهید، سپس نسخه دائمی را در Dockerfile یا فایل compose قرار دهید.