كيف يدمج Docker Compose عدة ملفات؟
تعرّف إلى تحميل compose.override.yaml تلقائياً، وترتيب دمج الملفات، وفخ ports الذي يُبقي المنفذ مفتوحاً، واستخدام include لفصل dev وprod.
ما الذي يفعله Compose عند استخدام أكثر من ملف
يمكن لـDocker Compose إنشاء مشروع واحد من عدة ملفات. يقرأ الملفات بالترتيب الذي يتلقاه به، ويدمجها في نموذج واحد؛ لذلك تكون قيمة الملف اللاحق هي المعتمدة عند تعارض أي قيمة. توجد آليتان لتنفيذ ذلك من سطر الأوامر: ملف override يحمّله Compose تلقائياً، والعَلَم -f الذي تمرره يدوياً. توجد آلية ثالثة داخل الملف نفسه، وهي العنصر include، وتعمل بطريقة مختلفة عن الآليتين السابقتين.
لا تتم عملية الدمج باستبدال بسيط. تندمج التعيينات مفتاحاً بمفتاح، وتُلحق التسلسلات، بينما تُستبدل مجموعة صغيرة من الحقول بالكامل. ينشأ معظم الالتباس من هذا الاختلاف، وقائمة ports هي التي تفاجئ الجميع تقريباً.
يفترض كل ما يلي استخدام Compose v2 أو إصداراً أحدث، مع إضافة docker compose بدلاً من البرنامج النصي القديم docker-compose. الإصدارات الحالية مرقمة v5، وتعمل الإضافة بالطريقة نفسها. شغّل docker compose version للتحقق. إذا لم تكتب ملف Compose بعد، فابدأ بـدليل أساسيات Docker Compose ثم عُد إلى هنا.
ملف التجاوز الذي يحمّله Compose تلقائياً
شغّل docker compose up من دون الخيار -f، وسيبحث Compose في دليل العمل ثم في الأدلة الأصلية عن compose.yaml أو docker-compose.yaml. إذا وُجد ملف تجاوز بجوار الملف الأساسي، فسيحمّله Compose بعده تلقائياً.
ls compose.yaml compose.override.yaml
docker compose up -dعند وجود الملفين، تكون النتيجة مماثلة لكتابتهما يدوياً.
docker compose -f compose.yaml -f compose.override.yaml up -dيتعرّف Compose على الأسماء compose.override.yaml وcompose.override.yml، وكذلك الاسمين الأقدمين docker-compose.override.yml وdocker-compose.override.yaml. أما أي اسم آخر، مثل compose.dev.yaml، فلا يُحمّل إلا عند تحديده باستخدام -f.
بمجرد تمرير -f واحد، يتوقف التحميل التلقائي. يقرأ docker compose -f compose.yaml up ذلك الملف وحده ويتجاهل ملف التجاوز. وهذه هي الخاصية التي يعتمد عليها نمط dev وprod الوارد لاحقاً في هذا الدليل.
لهذا الأمر أثر إيجابي وسلبي على الخادم. يُحمَّل ملف override المتروك في مجلد النشر مع كل أمر docker compose مجرد يُنفَّذ من ذلك المجلد، بما في ذلك الأمر الذي يشغّله cron. هكذا تنتهي حزمة تشغيل في بيئة الإنتاج بربط مجلد الشفرة المصدرية عبر bind mount، رغم أن أحداً لم يقصد تضمينه في الإصدار. شغّل docker compose config بعد كل عملية نشر، واقرأ الناتج. عندما تكون عملية النشر غير تفاعلية، لا يفيد هذا التحقق إلا إذا أبلغك أحد بحدوث خطأ. وهذه مهمة قناة دفع مثل خادم ntfy مستضاف ذاتياً يمكن لمهمة cron أو وحدة systemd من النوع OnFailure النشر إليها.
ترتيب الملفات باستخدام -f ومكان حل المسارات النسبية
ينشئ Compose الإعدادات بالترتيب الذي تقدّم به الملفات. وتتجاوز الملفات اللاحقة إعدادات الملفات السابقة وتضيف إليها. من اليسار إلى اليمين، تكون الأولوية للملف الأخير.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dيجب أن تستخدم كل أوامر المشروع نفسه قائمة الملفات نفسها. إذا شغّلت up مع ملفين وlogs مع ملف واحد، فأنت تتعامل مع نموذج مدمج مختلف. وهذه طريقة سريعة للوصول إلى خدمة يقول Compose إنها غير موجودة. تزداد المخاطر مع حزمة تُنفَّذ ترقياتها باستخدام أوامر لمرة واحدة، مثل خطوة ترحيل قاعدة البيانات في مكتب دعم Chatwoot مستضاف ذاتياً، حيث يستهدف docker compose run يُنفَّذ بقائمة ملفات خاطئة نموذجاً مختلفاً بهدوء عن النموذج الذي تستخدمه خدماتك حالياً. عيّن القائمة مرة واحدة بدلاً من ذلك باستخدام متغير البيئة COMPOSE_FILE.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dالفاصل هو : على Linux، ويغيّره COMPOSE_PATH_SEPARATOR. ويمكن أن يوجد COMPOSE_FILE أيضاً في ملف المشروع .env، وبذلك يصبح جزءاً من مستودع المشروع بدلاً من أن يبقى في سجل أوامر shell. تتغلب أي قيمة تعيّنها صراحةً في سطر الأوامر على قيمة متغير البيئة.
إليك الآن القاعدة التي تسبب مشكلات في bind mounts. عند استخدام عدة ملفات مع -f، تُحل جميع المسارات النسبية في جميع هذه الملفات بالاستناد إلى دليل الملف الأول، لا إلى الدليل الذي يوجد فيه الملف الذي يحتوي على المسار. إذا كتبت ./data:/var/lib/postgresql/data داخل deploy/prod/compose.prod.yaml، فسيبحث Compose عن ./data بجوار الملف الأساسي. بعد ذلك ينشئ Docker دليلاً فارغاً في ذلك المسار الخاطئ، وتبدأ الحاوية من دونه أي محتوى. يبدو الأمر كفقدان للبيانات، لكنه ليس كذلك. مرّر --project-directory لتعيين المسار الأساسي بنفسك، أو استخدم include، الذي يحل كل ملف بالاستناد إلى دليله الخاص.
يأتي اسم المشروع من الدليل الأساسي نفسه. لذلك، قد يؤدي تغيير الملف الأول إلى إعادة تسمية المشروع. تعني إعادة تسمية المشروع أسماء حاويات جديدة وأسماء volumes جديدة، بينما يبقى volume القديم على القرص تحت الاسم القديم. ثبّت الاسم بدلاً من ذلك باستخدام name: على المستوى الأعلى في الملف الأساسي.
name: myappالحقول التي تُدمج والحقول التي تُستبدل
تدمج Compose القيم وفق نوعها، لا وفق اسم الحقل.
- تُستبدل الحقول ذات القيمة المفردة.
imageوcommandوentrypointوmem_limitتأخذ القيمة اللاحقة بالكامل. لا يمكنك إلحاق وسيطة واحدة بـcommand، لأن التجاوز يعيد كتابة السطر بأكمله. - تُدمج التعيينات مفتاحاً بمفتاح.
environmentوlabelsوvolumesوdevicesتحتفظ بكل مفتاح من كلا الملفين، ويكون الملف اللاحق هو المعتمد لأي مفتاح موجود في الملفين. بالنسبة إلىenvironmentوlabels، يكون المفتاح هو اسم المتغير أو التسمية. وبالنسبة إلىvolumesوdevices، يكون المفتاح هو مسار الحاوية. - تُلحَق المتتاليات. تُدمج
dnsوdns_searchوexposeوtmpfsوexternal_linksبالتسلسل. فإذا كان الملف الأساسي يحتوي علىexpose: ["3000"]وكان ملف التجاوز يحتوي على["4000", "5000"]، تكون النتيجة["3000", "4000", "5000"].
تحتوي أربع متتاليات على مفتاح تعريف، لذلك تُدمج العناصر المتطابقة وفق هذا المفتاح بدلاً من إلحاقها. تطابق volumes وsecrets وconfigs وفق target. أما ports فتطابق وفق مجموعة ip وtarget وpublished وprotocol.
اقرأ قاعدة ports مرتين، لأنها موضع الخطأ الشائع. لا يُعد إدخالان للمنفذ الإدخال نفسه إلا عندما تتطابق الأجزاء الأربعة كلها. إذا غيّرت أي جزء منها، ترى Compose منفذاً ثانياً غير مرتبط بالأول، ولذلك تحتفظ بكليهما.
لماذا يظل منفذك منشوراً بعد استخدام ملف override
يحتوي الملف الأساسي على إعداد ينشر خدمة على جميع الواجهات:
services:
web:
image: nginx:1.27
ports:
- "8080:80"يحتوي ملف override على إعداد لربطها بـlocalhost فقط، لأن Reverse Proxy سيعمل أمامها:
services:
web:
ports:
- "127.0.0.1:8080:80"تحقق من النتيجة قبل افتراض أن الإعداد نجح.
docker compose -f compose.yaml -f compose.prod.yaml configيظهر الإدخالان في الناتج. يختلف الجزء ip، أي 0.0.0.0 مقابل 127.0.0.1، ولذلك يتعامل الدمج معهما كمنفذين مختلفين. يظل الربط العام الذي حاولت إزالته موجوداً في النموذج. وهذا أكثر أهمية مع Docker من غيره، لأن المنفذ المنشور يُكتب في iptables قبل قواعد جدارك الناري. يشرح سبب تجاوز منافذ Docker المنشورة لـufw هذه الآلية.
هناك إصلاحان. الإصلاح الصريح هو وسم !override، الذي يستبدل السمة كاملة ويتجاوز قواعد الدمج:
services:
web:
ports: !override
- "127.0.0.1:8080:80"يتطلب !override إصدار Compose v2.24.4 أو أحدث. أما الإصلاح المحمول فلا يحتاج إلى أي وسم: اترك ports خارج الملف الأساسي تماماً، وعرّفه فقط في الملفات الخاصة بكل بيئة. عندما لا يوجد شيء لدمجه، لا يوجد شيء يمكن أن يتسرّب. هذا هو النمط المستخدم في المثال التطبيقي أدناه.
حذف قيمة من مجموعة الإعداد الأساسية
تزيل !reset إحدى السمات، وتعيدها إلى قيمتها الافتراضية أو إلى القيمة الفارغة. وهي تتطلب قيمة وتتجاهلها، لذلك اكتب قيمة صحيحة وفارغة.
services:
web:
ports: !reset []
environment:
DEBUG: !reset nullتعمل !reset مع Compose v2.18.0 أو أحدث، أي قبل عدة إصدارات من وصول !override. استخدمها عندما لا يكون ملف الإعداد الأساسي ملكاً لك، مثل جزء إعدادات تابع لمورّد تستورده. وتُعد حزمة منشورة من المشروع الأصلي مثالاً واضحاً على ذلك: يعرّف ملف Compose الذي يقف خلف مساحة عمل AFFiNE ذاتية الاستضافة أربع حاويات لم تكتبها أنت، ويتيح لك !reset مسح سمة واحدة في إحداها دون إنشاء نسخة متفرعة من الملف وتحمل مسؤولية تتبعه.
include، للمكدسات المجمّعة من أجزاء
يسحب include تطبيق Compose آخر إلى نموذجك. وهو عنصر على المستوى الأعلى، وليس علَماً.
include:
- path: ../commons/compose.yamlيُحمَّل كل مسار في include كنموذج تطبيق Compose مستقلاً، مع دليل مشروع خاص به. لذلك تُحلّ المسارات النسبية داخل ذلك الملف بالنسبة إلى دليله هو. وهذا هو الفرق الفعلي عن -f، وهو سبب كون include الأداة المناسبة عندما يكون الجزء في مجلد آخر أو في مستودع آخر. هذا هو الشكل المعتاد لمكدس مورّد لم تكتبه أنت: يمكن أن يوجد ملف Compose متعدد الخدمات الذي يقف خلف تثبيت Authentik SSO مستضاف ذاتياً في دليله الخاص، مع بقاء مساراته النسبية سليمة، بينما يظل ملفك متعلقاً بخدماتك أنت.
يقبل الشكل المطوّل خيارات فرعية.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envيقبل path قائمة، وتُدمج هذه الملفات معاً وفق القواعد العادية قبل أن تنضم النتيجة إلى نموذجك. يحدد project_directory المسار الأساسي المستخدم لحل المسارات النسبية في الملف المضمَّن. يحدد env_file الملف الذي يستمد منه ملف Compose المضمَّن القيم الافتراضية للاستبدال. وتكون قيمته الافتراضية هي .env في دليل مشروع ذلك الملف، مع بقاء بيئة مشروعك المحلي ذات أولوية عليه. وبهذا يمكنك تجاوز أحد متغيراته دون تعديل الجزء. يتطلب include الإصدار Compose v2.20.0 أو أحدث. وتنطبق الخيارات نفسها على إضافة تعمل في حاوية واحدة إلى مكدس تشغّله مسبقاً، مثل Halcyon، الذي يعيد تصميم مكتبة Jellyfin لتبدو كمتجر تأجير من التسعينيات: يحتفظ ملفها بوسم image الخاص به وبـenv_file الخاص به، لذلك لا تتطلب ترقيته لمس ملف مكدس الوسائط لديك.
تذكر وثائق Docker أن اسم المورد المعرّف في ملفك وفي ملف مضمَّن يُرفض باعتباره تعارضاً. لكن أداة التحميل في الإصدار الحالي لا تفعل ذلك: فاعتباراً من Compose v5.6.0، وبعد التحقق في 2026-10-11، تدمج التعريفين، مع إعطاء الأولوية لملفك عند تعارض أي قيمة، بدلاً من التوقف مع ظهور خطأ. لذلك لا تعتمد على أن الاسم المكرر سيفشل بوضوح. لتغيير شيء يعرّفه ملف مضمَّن، ضع التغيير في compose.override.yaml: يُطبَّق التجاوز على النموذج المجمَّع، فيكون التغيير صريحاً ولا يعتمد على كيفية تعامل إصدار معين مع التعارض. وتفيد هذه العادة أكثر مع مكدس يُعادَت كتابة ملفه المصدر upstream مع كل إصدار، مثل خوادم الصور متعددة الحاويات التي تجري مقارنتها في PhotoPrism مقارنةً بـ Immich، حيث يجب أن يكون ربط localhost أو volume إضافي في ملف التجاوز، لا في الملف الذي ستستبدله الترقية التالية.
باختصار: يركّب include تطبيقات منفصلة، بينما يضيف -f طبقات إعدادات إلى تطبيق واحد.
تقسيم بيئتي التطوير والإنتاج على VPS واحد
إليك النمط الكامل في ثلاثة ملفات. يعرّف الملف الأساسي ما ينطبق في كل مكان، ولا ينشر أي منافذ.
name: myapp
services:
app:
image: ghcr.io/example/app:1.4.2
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:الشرط depends_on هو ما يجعل التطبيق ينتظر قاعدة بيانات تستجيب، لا مجرد حاوية موجودة، كما هو موضح في فحوصات الصحة وشروط depends_on. تُستبدل قيمة POSTGRES_PASSWORD من ملف المشروع .env، وهذا الملف لا ينتمي مطلقاً إلى git. راجع ملفات البيئة وأسرار Compose للاطلاع على الخيارات الأكثر أماناً.
بعد ذلك يأتي compose.override.yaml، وهو ملف يحمّله Compose تلقائياً. هذا هو ملف المطوّر.
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
volumes:
- ./src:/app/src
db:
ports:
- "127.0.0.1:5432:5432"على الحاسوب المحمول، يدمج الأمر docker compose up المجرد هذين الملفين. يستبدل command القيمة الافتراضية للصورة لأنه ذو قيمة مفردة. ويستبدل LOG_LEVEL القيمة info لأن environment يدمج القيم بحسب المفتاح. أما نقطة الربط والمنفذان المنشوران فهما إضافتان فقط، ويُربط منفذ قاعدة البيانات بـlocalhost حتى لا يعرض حاسوب محمول على شبكة مشتركة PostgreSQL لبقية الشبكة.
أخيراً، compose.prod.yaml. اسمه ليس من الأسماء التي يبحث عنها Compose، لذلك لا يُحمّل عن طريق الخطأ.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512Mعلى VPS، تسمّي الملفين معاً، وتسمّي الملفات بهذه الطريقة تحديداً ما يستبعد ملف التجاوز.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml psينبغي أن يعرض ps الخدمتين قيد التشغيل، مع إظهار db للقيمة (healthy). وبما أنك مرّرت -f، فلم تتم قراءة compose.override.yaml. لذلك لا يمكن لأمر التطوير، أو نقطة ربط المصدر، أو المنفذ العام 3000 الوصول إلى الإنتاج، رغم وجود الملف في الدليل نفسه. أما المنفذ 8000 فمربوط بـlocalhost فقط، وهو جاهز لـproxy. راجع تشغيل عدة تطبيقات خلف Traefik عند إضافة الخدمة الثانية.
اضبط COMPOSE_FILE=compose.yaml:compose.prod.yaml في .env على الخادم، وستعود بقية أوامرك إلى استخدام docker compose logs -f app العادي.
يحافظ مكدس الخدمة الواحدة على البنية نفسها، لأن متتبع تمارين openGym مستضاف ذاتياً يجب أن يستجيب عبر TLS خلف proxy قبل تسجيل أول passkey، كما أن الملف الأساسي الذي لا يحتوي على ports هو ما يمنع ربطاً عاماً عارضاً من تجاوز proxy والوصول إلى الخدمة أولاً.
اقرأ النموذج المدمج قبل النشر
يطبع docker compose config النموذج المدمج بالكامل، بعد استبدال جميع المتغيرات. هذه ليست معاينة. هذا هو الإدخال الفعلي الذي سيعمل عليه Compose. لذلك، عندما يختلف الناتج عما تتوقعه، يكون الناتج هو الصحيح.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --servicesيُبقي --no-interpolate ${VAR} دون استبدال. استخدمه قبل لصق الناتج في أي مكان، لأن config العادي يطبع كل سر بعد حله بنص واضح. يسرد --services أسماء الخدمات فقط، وهي طريقة سريعة للتأكد من أن include أدرج ما تتوقعه.
أنماط الفشل وما ستراه
no configuration file provided: not found. لم يعثر Compose على أي شيء لقراءته. أنت خارج دليل المشروع، أو أن COMPOSE_FILE يشير إلى مسار غير موجود. يبحث Compose في الأدلة الأصلية عن الملف الأساسي الافتراضي، لكنه لا يبحث في أي مكان عن ملف سمّيته بنفسك.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. تتم مطابقة القيم المتغيرة مقابل ملف .env الخاص بالمشروع وبيئة shell. ودليل المشروع هنا هو دليل أول ملف -f. يؤدي النشر من دليل يختلف عن الدليل الذي يحتوي على .env إلى ظهور هذا التحذير، ثم إلى قاعدة بيانات ترفض كل اتصال.
لا يظهر التعديل الذي أجريته على ملف override في docker compose config. إما أنك مرّرت -f، ما يعطّل التحميل التلقائي لملف override، أو أن Compose عثر على compose.yaml في دليل أصلي، وملف override الخاص بك ليس بجواره. يخبرك تشغيل docker compose config دون أي وسيطات أخرى بالنموذج الذي يبنيه Compose فعلياً.
يكون bind mount فارغاً، وينشئ Docker دليلاً لم تطلبه. حُلّ المسار النسبي بالاستناد إلى دليل الملف الأول. صحّح المسار، أو مرّر --project-directory، أو انقل الجزء إلى ما بعد include.
تعود الحاويات بأسماء جديدة، ويبدو أحد volumes فارغاً. تغيّر اسم المشروع لأن اسم المشروع يتبع دليل الملف الأول. أضف name: على مستوى الملف الأساسي، وسيتوقف تغيّر الأسماء. لا يزال volume القديم موجوداً تحت البادئة القديمة، وسيعرضه docker volume ls.
لا يزال أحد المنافذ التي أزلتها في override مفتوحاً. أدّى دمج ports إلى الإلحاق بدلاً من الاستبدال. أكّد ذلك باستخدام docker compose config، ثم استخدم !override أو انقل ports خارج الملف الأساسي.
FAQ
هل يحمّل Compose الملف compose.override.yaml تلقائياً؟
نعم، عند تشغيل docker compose من دون راية -f. يبحث Compose في دليل العمل والأدلة الأصلية عنهما عن compose.yaml أو docker-compose.yaml. وإذا كان ملف override موجوداً بجانبه، فسيحمّله ثانياً. الأسماء المعترف بها هي compose.override.yaml وcompose.override.yml وdocker-compose.override.yml وdocker-compose.override.yaml. يؤدي تمرير أي -f إلى تعطيل هذا السلوك، ولذلك يقرأ docker compose -f compose.yaml up ملفاً واحداً فقط.
بأي ترتيب تُدمج عدة ملفات -f؟
من اليسار إلى اليمين. ينشئ Compose الإعداد بالترتيب الذي تقدّم به الملفات. ويستبدل كل ملف الإعدادات التي سبقته ويضيف إليها، ولذلك يفوز الملف الأخير في السطر عند حدوث أي تعارض. يجب استخدام القائمة نفسها مع كل أمر في ذلك المشروع. وهذا هو الغرض من COMPOSE_FILE=compose.yaml:compose.prod.yaml.
لماذا ما يزال المنفذ منشوراً بعد استبداله؟
لأن إدخالات ports تُحدَّد بمجموعة ip وtarget وpublished وprotocol كاملة. يختلف استبدال 127.0.0.1:8080:80 في ملف أساسي يحتوي على 8080:80 في الجزء ip. لذلك يتعامل Compose معه باعتباره منفذاً ثانياً ويُبقي الإدخالين. شغّل docker compose config، وسترى الإدخالين. استخدم ports: !override في Compose v2.24.4 أو إصدار أحدث، أو اترك ports خارج الملف الأساسي حتى لا يوجد إدخال يندمج معه.
ما الفرق بين include و -f؟
يضم -f عدة ملفات في تطبيق واحد. ويُحل كل مسار نسبي في كل ملف بالنسبة إلى دليل الملف الأول. أما include فيضم تطبيق Compose منفصلاً. ويحتفظ كل مسار مضمّن بدليل مشروعه، ولذلك تُحل مساراته النسبية بالنسبة إلى ذلك الدليل. استخدم -f لطبقات البيئة الخاصة بمكدسك، واستخدم include لمقطع تتم صيانته في مكان آخر. يتطلب include Compose v2.20.0 أو إصداراً أحدث.
كيف أزيل قيمة يحددها الملف الأساسي؟
استخدم الوسم !reset في Compose v2.18.0 أو إصدار أحدث. اكتب ports: !reset [] أو MY_VAR: !reset null في الملف الذي يستبدل الإعداد. عندها تعود السمة إلى قيمتها الافتراضية أو إلى null. القيمة التي تقدّمها إلى الوسم مطلوبة، لكن لا يتم استخدامها. إذا أردت استبدال سمة بدلاً من مسحها، فاستخدم !override. ويتطلب ذلك v2.24.4 أو إصداراً أحدث.