SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-28

كيف يدمج Docker Compose عدة ملفات؟

تعرّف إلى تحميل compose.override.yaml تلقائياً، وترتيب دمج الملفات، وفخ ports الذي يُبقي المنفذ مفتوحاً، واستخدام include لفصل dev وprod.

ما الذي يفعله Compose عند استخدام أكثر من ملف

يمكن لـDocker Compose إنشاء مشروع واحد من عدة ملفات. ويقرأ هذه الملفات بالترتيب الذي يتلقاها به، ثم يدمجها في نموذج واحد، لذلك تكون قيمة الملف اللاحق هي المعتمدة عند تعارضها مع قيمة سابقة. توجد آليتان لتنفيذ ذلك من سطر الأوامر: ملف override يحمّله Compose تلقائياً، والعَلَم -f الذي تمرّره يدوياً. وتوجد آلية ثالثة داخل الملف نفسه، وهي العنصر include، لكنها تعمل بطريقة مختلفة عن الآليتين السابقتين.

لا يكون الدمج مجرد استبدال مباشر. تُدمج الخرائط مفتاحاً بمفتاح، وتُضاف عناصر التسلسلات، بينما تُستبدل مجموعة صغيرة من الحقول بالكامل. وينشأ معظم السلوك غير المتوقع من هذا الفرق، وتحديداً من قائمة ports التي تسبب الالتباس لمعظم المستخدمين.

يفترض كل ما يلي استخدام Compose v2، أي إضافة docker compose، وليس البرنامج النصي القديم docker-compose. شغّل 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 إحدى السمات، فتعيدها إلى قيمتها الافتراضية أو إلى null. يأخذ الأمر قيمة ويتجاهلها، لذلك اكتب قيمة صحيحة وفارغة.

services:
  web:
    ports: !reset []
    environment:
      DEBUG: !reset null

يتطلب !reset الإصدار 2.24 من Compose أو إصداراً أحدث. استخدمه عندما لا يكون الملف الأساسي قابلاً للتعديل بواسطتك، مثل جزء إعدادات يوفّره مورّد وتضمّنه. وتندرج حزمة مكدس منشورة من المصدر الأصلي ضمن هذه الحالة تماماً: يعرّف ملف Compose وراء مساحة عمل AFFiNE مستضافة ذاتياً أربع حاويات لم تكتبها أنت، ويتيح لك !reset مسح سمة واحدة من إحدى هذه الحاويات دون إنشاء fork للملف وتحمل مسؤولية تتبعه.

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 الملف المضمّن متغيراته الخاصة للاستبدال، ما يمنع الجزء المشترك من قراءة .env الخاص بمشروعك دون قصد. يتطلب include الإصدار Compose v2.20.0 أو إصداراً أحدث. وتنطبق الخيارات نفسها على إضافة تعمل في حاوية واحدة إلى مكدس تشغّله مسبقاً، مثل Halcyon، الذي يعيد تصميم مكتبة Jellyfin لتبدو كمتجر تأجير من حقبة 90s: يحتفظ ملفه بوسم image الخاص به وبـenv_file الخاص به، لذلك لا تتطلب ترقيته تعديل الملف الذي يعتمد عليه مكدس الوسائط لديك.

يُبلّغ عن تكرار أسماء الموارد بين ملفك والملف المضمّن باعتباره خطأ، بدلاً من دمجها بصمت، وهذا مقصود. لتغيير شيء يعرّفه ملف مضمّن، ضع التغيير في compose.override.yaml: يُطبَّق التجاوز على النموذج المجمّع، ولذلك يمكنه تعديل الموارد المضمّنة من دون التعارض معها. وتظهر فائدة هذه الممارسة خصوصاً مع مكدس يُعاد فيه كتابة ملف المنبع عند كل إصدار، مثل خوادم الصور متعددة الحاويات التي جرت مقارنتها في PhotoPrism مقابل Immich، حيث يجب وضع binding محلي أو 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.24 أو أحدث. اكتب ports: !reset [] أو MY_VAR: !reset null في ملف التجاوز، فتعود السمة إلى قيمتها الافتراضية أو إلى null. القيمة التي تقدّمها إلى الوسم مطلوبة، لكن يجري تجاهلها. إذا أردت استبدال سمة بدلاً من مسحها، فاستخدم !override. ويتطلب ذلك الإصدار v2.24.4 أو أحدث.