docker compose exec: افتح shell تفاعلياً داخل الحاوية
تعلّم فتح shell داخل خدمة Compose قيد التشغيل باستخدام docker compose exec، ومتى تستخدم run --rm لخدمة متوقفة أو لتجنب التأثير في الحاوية الحالية.
احصل على shell تفاعلي باستخدام docker compose exec
يفتح docker compose exec web bash shell تفاعلياً داخل الحاوية التي تعمل بالفعل بوصفها الخدمة web. الاسم الذي يأتي بعد exec هو اسم الخدمة الوارد في compose.yaml، وليس اسم الحاوية. إذا لم تتضمن الصورة bash، فاطلب sh بدلاً منه.
docker compose ps
docker compose exec web bashشغّل docker compose ps أولاً. يجب أن يعرض web بالحالة running. بعد ذلك، يضعك الأمر الثاني عند موجه داخل الحاوية، ويعيدك exit أو Ctrl-D إلى الخادم المضيف. تستمر الخدمة في العمل بعد خروجك، لأن exec بدأ عملية ثانية بجانب العملية الرئيسية. لا يؤدي إغلاق shell إلى التأثير في PID 1 (معرّف العملية 1)، وهي العملية التي أُنشئت الحاوية لتشغيلها.
هذه إحدى طريقتين للدخول. ينضم exec إلى حاوية موجودة بالفعل. ينشئ docker compose run حاوية جديدة من تعريف الخدمة نفسه. تنطلق معظم التفاصيل الأخرى في هذا الدليل من هذا الفرق الأساسي.
لماذا يكون -it اختيارياً في Compose لكنه مطلوباً مع docker العادي
يتحكم خياران في الجزء التفاعلي من الجلسة. يُبقي -i الإدخال القياسي مفتوحاً، لذلك تصل الكتابة التي تُدخلها إلى العملية. يخصّص -t طرفية زائفة، تُسمّى TTY، كي تطبع الصدفة موجّه الأوامر وتتعامل مع مفاتيح الأسهم. يترك docker exec الخيارين معطّلين افتراضياً، ولذلك يكتب كل مثال رأيته docker exec -it. يفعّل docker compose exec الخيارين تلقائياً، لذلك ينفّذ docker compose exec -it web bash وdocker compose exec web bash الشيء نفسه. وما زال Compose يقبل -it حتى تستمر العادة القديمة في العمل.
ستلاحظ غياب TTY خلال ثوانٍ. تعمل الصدفة، لكنها لا تطبع موجّه الأوامر، ولا يصل Ctrl-C إلى العملية. أما الحالة المعاكسة، حيث يجب أن تطلب من Compose عدم تخصيص TTY، فلها خيارها الخاص وقسمها الخاص في موضع لاحق.
ما يجب فعله عندما لا تحتوي الصورة على bash
عند طلب bash من صورة مبنية على Alpine، يفشل exec كما يلي:
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknownهذه الرسالة لا تشير إلى مشكلة في exec. بل تفيد بأن الملف التنفيذي الذي طلبته غير موجود في الصورة. تتضمن Alpine BusyBox، الذي يوفر ash باسم /bin/sh، ولا تتضمن bash إطلاقاً. لذلك اطلب sh:
docker compose exec web shتتضمن الصور المبنية على Debian وUbuntu، بما فيها الوسوم -slim، bash. ويوفّر bash سجل الأوامر وإكمالاً أفضل. لذلك جرّب bash أولاً، ثم استخدم sh عند الفشل. يوجد sh في كل صورة تقريباً من الصور العامة الغرض.
لا تتضمن بعض الصور أي shell إطلاقاً. تحتوي الصور Distroless والصور المبنية FROM scratch على الملف التنفيذي للتطبيق ومكتبته فقط، وذلك عمداً، لأن shell غير الموجود لا يمكن استغلاله ضدك. في هذه الصور، يفشل sh بالرسالة نفسها، ولا يبقى شيء آخر لتجربته. توجد طريقتان عمليتان. تنشر صور Distroless من Google وسوماً :debug تضيف shell من BusyBox، لذا يمكنك تبديل الوسم مؤقتاً للدخول إلى الصورة. أو ابدأ حاوية منفصلة داخل مساحات أسماء الحاوية المستهدفة:
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، وليس إلى التطبيق. وبما أن مساحة أسماء العمليات مشتركة، يصل 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، باستخدام الصورة والبيئة ووحدات التخزين والشبكات نفسها، ويستبدل أمر الخدمة بالأمر الذي كتبته. يحذف --rm هذه الحاوية عند الخروج. إذا حذفت --rm، فستتراكم الحاويات المتبقية بأسماء مثل myproject-web-run-4f1c2b، وسيعرضها لك docker compose ps -a، ولن ينظفها أي شيء آخر.
يفاجئ سلوكان في run بعض المستخدمين. فهو لا ينشر منافذ الخدمة إلا إذا أضفت --service-ports، وهذا مقصود: إذ ستفشل حاوية ثانية تحاول ربط المنفذ 8080 على المضيف بينما لا تزال الحاوية الأولى تستخدمه، مع bind: address already in use. كما أنه يبدأ كل ما تسرده الخدمة ضمن depends_on قبل ظهور الصدفة، لذلك قد يؤدي الفحص السريع من الداخل إلى بدء قاعدة بيانات وذاكرة تخزين مؤقت. يتجاوز --no-deps ذلك.
يمر run عبر ENTRYPOINT الخاص بالصورة، بينما لا يفعل exec ذلك. يبدأ exec الأمر الذي تحدده مباشرةً داخل الحاوية الموجودة، لذلك لا يرى سكربت نقطة الدخول هذا الأمر. أما مع run، فيصل bash إلى ذلك السكربت كوسائط. تنهي عدة صور رسمية نقطة الدخول باستخدام exec "$@"، لذلك يمر الأمر مباشرةً وتحصل على الصدفة. أما السكربت الذي يفسر وسائطه بنفسه فسيتعامل معها بطريقة أخرى، وعندها تستبدل نقطة الدخول لذلك التشغيل وحده:
docker compose run --rm --entrypoint sh webهذا هو السبب الأكثر شيوعاً لاختلاف سلوك أمر يعمل مع exec عند تشغيله مع run. ويوضح الفصل بين الأمر ونقطة الدخول أي جزء من إعداد الصورة تستبدله في كل مرة.
exec أو run: كيف تختار
- يتطلب
execحاوية قيد التشغيل. أماrunفلا يتطلب ذلك، وقد يبدأ التبعيات. - يرى
execقائمة العمليات الفعلية والملفات الحالية، بما في ذلك كل ما كتبته التطبيق منذ بدء تشغيله. أماrunفيحصل على نسخة نظيفة من الصورة، لذلك لا يتضمن أياً من ذلك. - يتجاوز
execنقطة الدخول. أماrunفيشغّلها. - يترك
runحاوية بعد انتهائه ما لم تمرّر--rm.
استخدم exec لمعرفة ما يحدث فعلياً. استخدم run --rm للحصول على نسخة مؤقتة من البيئة نفسها، أو لتنفيذ أمر ترحيل لمرة واحدة، أو عندما لا تبقى الخدمة الفعلية قيد التشغيل مدة كافية لتنفيذ exec داخلها.
أعلام exec المفيدة: المستخدم، ودليل العمل، والنسخ المتماثلة
تنتقل معظم الصور إلى مستخدم غير root، لذلك يتوقف تثبيت أداة تشخيص داخل جلسة exec عند هذه النقطة:
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)يمنحك -u root جلسة shell بصلاحيات root داخل الحاوية نفسها:
docker compose exec -u root web shيحدّد -w /srv/app دليل العمل لهذا الأمر فقط. يضيف -e KEY=value متغير بيئة إلى جلستك، وليس إلى الخدمة. عندما تشغّل الخدمة أكثر من نسخة متماثلة واحدة، يحدّد --index 2 الحاوية التي ستتصل بها. إذا كنت تبحث عن سبب ملكية الملفات في دليل موصول، يوضح PUID وPGID في صور الحاويات سبب تحديد المعرّفات الرقمية، لا أسماء المستخدمين، للجهة التي يمكنها الكتابة فيه.
افتح صدفة psql أو mysql داخل حاوية قاعدة البيانات
يكون العميل موجوداً بالفعل داخل صورة قاعدة البيانات، لذلك لا تحتاج إلى تثبيته على المضيف، ولا تحتاج إلى نشر المنفذ:
docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -pتتضمن صور Postgres الأمر psql، وتتضمن صور MySQL الأمر mysql، وتتضمن صور MariaDB الأمر mariadb. يُنشأ الاتصال من داخل الحاوية، لذلك يعمل هذا حتى عندما لا ينشر ملف Compose أي منفذ لقاعدة البيانات. هذا هو الإعداد الأكثر أماناً: لا يستطيع أي شيء على الإنترنت الوصول إلى منفذ لم تنشره.
هناك خطأ واحد يكلّف البعض ساعات من العمل. توسّع صدفتك المتغيرات على المضيف قبل أن يرى Docker الأمر، لذلك يرسل -U "$POSTGRES_USER" سلسلة فارغة عندما يكون هذا المتغير موجوداً داخل الحاوية فقط. تضع علامات الاقتباس المفردة والصدفة داخل الحاوية عملية التوسيع في المكان الصحيح:
docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'لا تستخدم docker compose run --rm db من دون أمر هنا. فهذا يشغّل خادم Postgres ثانياً على وحدة تخزين البيانات نفسها، ويرفض الخادم البدء:
FATAL: lock file "postmaster.pid" already existsيؤدي ملف القفل وظيفته، لأن خادمين يكتبان في دليل بيانات واحد قد يتسببان في تلفه. أثناء تشغيل قاعدة البيانات، نفّذ الأمر داخل الحاوية قيد التشغيل. أما قرار وضع قاعدة البيانات في Compose من عدمه فهو قرار منفصل، ويعرض تشغيل قاعدة البيانات في Docker أو على المضيف المفاضلة بين الخيارين.
الخدمات التي تحتاج إلى وحدة تحكم عند بدء التشغيل: stdin_open وtty
تغطي أوامر exec وrun جلسات الصدفة التي تفتحها يدوياً. أما الخدمة التي تكون عمليتها الرئيسية تفاعلية بطبيعتها فتحتاج إلى مفتاحين في ملف compose:
services:
console:
image: python:3.12-slim
command: python
stdin_open: true
tty: truestdin_open: true هي docker run -i وtty: true هي docker run -t. من دونهما تبدأ الحاوية ثم تخرج فوراً برمز 0، ويعرض docker compose ps -a القيمة Exited (0). لم يحدث أي تعطل. تقرأ python، عند عدم وجود طرفية على stdin، نهاية الملف فوراً وتخرج بصورة طبيعية. وهذا هو السلوك الصحيح لبرنامج لا يكتب أحد فيه.
عند ضبط المفتاحين، أرفق العملية قيد التشغيل:
docker attach $(docker compose ps -q console)افصل الاتصال باستخدام Ctrl-P ثم Ctrl-Q، وبذلك تترك العملية قيد التشغيل. يعمل هذا التسلسل فقط عندما تحتوي الحاوية على TTY ويكون stdin مفتوحاً. أما Ctrl-C فيرسل مقاطعة إلى PID 1 ويوقف الخدمة.
اترك المفتاحين معطلين للخدمات العادية. لا يقرأ خادم الويب stdin، كما أن tty: true يجعل كثيراً من البرامج تنتقل إلى إخراج ملوّن وتخزين مؤقت بحسب السطر لأنها تفترض أن شخصاً يراقبها، ما يملأ docker compose logs برموز الهروب.
لماذا يفشل exec المكتوب في سكربتات cron وCI: الخيار -T
يفشل أمر exec داخل مهمة cron أو مشغّل التكامل المستمر (CI)، رغم أنّه يعمل في الطرفية:
the input device is not a TTYيطلب Compose طرفية زائفة افتراضياً، ولا توفّر cron أي طرفية للمهمة. لذلك يفشل الطلب قبل تشغيل أمرك. يعطّل -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 كتابة تدفق البايتات أثناء خروجه، ولذلك يصل التفريغ المضغوط الذي يمر عبرها تالفاً. يحتاج أي إخراج معاد توجيهه أو ممرّر عبر pipe إلى -T.
هناك تفصيلان إضافيان متعلقان بـcron. مرّر -f باستخدام مسار مطلق، لأن cron تشغّل المهمة من الدليل الرئيسي، حيث لا يوجد ملف Compose. عندئذ يتوقف Compose مع no configuration file provided: not found. ويعيد exec رمز الخروج للأمر الذي شغّله، لذلك يؤدي فشل pg_dump إلى فشل سكربتك مع set -e، بدلاً من كتابة نسخة احتياطية فارغة والإبلاغ عن نجاح. جُمعت بقية الأوامر اليومية في ورقة مرجعية سريعة لأوامر Compose تستحق الاحتفاظ بها بجوار تلك السكربتات.
لماذا تختفي التغييرات التي تجريها داخل الحاوية
تثبّت أداة باستخدام exec، وتعدّل ملف إعداد، وتصلح المشكلة، ثم تختفي الإصلاحات بعد أسبوع. هذه هي آلية عمل طبقة الكتابة في الحاوية. يؤدي docker compose up -d، بعد أي تغيير في وسم الصورة أو تعريف الخدمة، إلى إتلاف الحاوية القديمة وإنشاء حاوية جديدة من الصورة، وتختفي كل التعديلات اليدوية مع الحاوية القديمة.
يختلف docker compose restart عن ذلك. فهو يوقف الحاوية نفسها ثم يبدأ تشغيلها، لذلك تبقى التعديلات اليدوية. ولهذا قد يبدو أن الإصلاح اليدوي يستمر لأسابيع، ثم يختفي أثناء تحديث غير مرتبط به. تبقى وحدات التخزين المسماة وعمليات الربط bind mounts بعد العمليتين، لأن بياناتها موجودة خارج الحاوية، ويوضح الربط bind mounts ووحدات التخزين المسماة كيفية اختيار النوع المناسب للبيانات التي تريد الاحتفاظ بها.
لذلك تعامل مع جلسة shell التي تبدأها باستخدام exec باعتبارها مكاناً للقراءة والاختبار. بعد معرفة الإصلاح، اكتب التغيير في موضع يبقى محفوظاً: أضف الحزمة إلى Dockerfile، والإعداد إلى ملف compose. ثم استخدم docker compose up -d لتطبيقه، وتحقق باستخدام exec آخر من أن الحاوية الجديدة تحتوي عليه فعلاً.
FAQ
ما الفرق بين docker compose exec وdocker compose run؟
ينفّذ exec أمراً داخل حاوية قيد التشغيل، إلى جانب العملية الرئيسية، ويتجاوز نقطة دخول الصورة. ينشئ run حاوية جديدة من تعريف الخدمة نفسه، باستخدام الصورة والبيئة ووحدات التخزين والشبكات نفسها، ويمرّر الأمر إلى نقطة الدخول، ويبدأ أي خدمات depends_on أولاً. كما لا ينشر run منافذ الخدمة، إلا إذا أضفت --service-ports. استخدم exec لفحص الخدمة قيد التشغيل. استخدم run --rm عندما تكون الخدمة متوقفة أو عندما لا تريد التأثير فيها.
لماذا يعرض docker compose exec رسالة تفيد بأن الخدمة ليست قيد التشغيل؟
يتصل exec بحاوية موجودة ولا يستطيع إنشاء حاوية، لذلك تُظهر الخدمة المتوقفة أو المنهارة service "web" is not running. افحص docker compose ps -a، الذي يعرض الحاويات التي خرجت مع حالة مثل Exited (1)، واقرأ docker compose logs web لمعرفة سبب توقفها. للحصول على shell رغم ذلك، شغّل docker compose run --rm --entrypoint sh web. ينشئ هذا الأمر حاوية جديدة من تعريف الخدمة نفسه، من دون تشغيل أمر البدء المعطّل.
كيف أفتح shell عندما لا تحتوي الصورة على bash؟
يعني فشل 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 إذا كان الناشر يوفره، أو ابدأ حاوية تصحيح في مساحات أسماء الهدف باستخدام docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot، حيث يأتي $CID من docker compose ps -q web.
لماذا يفشل أمر exec مع الرسالة "the input device is not a TTY" في cron؟
يطلب docker compose exec محطة طرفية زائفة افتراضياً، ولا يوفر cron أي محطة، لذلك يفشل الطلب قبل تشغيل أمرك. أضف -T لتعطيل ذلك: docker compose exec -T db pg_dump -U postgres app. استخدم -T أيضاً مع أي مخرجات معاد توجيهها أو عبر pipe، لأن TTY يغيّر تدفق البايتات ويتلف التفريغ الثنائي. في cron، مرّر أيضاً -f مع المسار المطلق لملف compose، وإلا يخرج Compose مع no configuration file provided: not found.
هل تستمر التغييرات التي أجريها داخل حاوية باستخدام exec بعد إعادة التشغيل؟
تستمر بعد docker compose restart، الذي يعيد استخدام الحاوية نفسها. وتُفقد بعد docker compose up -d عند حدوث أي تغيير في الصورة أو الإعدادات، لأن ذلك يعيد إنشاء الحاوية من الصورة ويتخلص من طبقتها القابلة للكتابة. تستمر البيانات المكتوبة في وحدات التخزين المسماة أو عمليات الربط bind mounts في الحالتين، لأنها موجودة خارج الحاوية. أجرِ التغييرات التشخيصية باستخدام exec، ثم ضع النسخة الدائمة في Dockerfile أو ملف compose.