SSD Nodes Learn Hosting plans →
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-31

שימוש במספר קבצי Docker Compose: מדריך למיזוג והגדרות

למדו כיצד Docker Compose ממזג קבצי YAML, מדוע compose.override.yaml נטען אוטומטית, ואיך להימנע מבעיות במיפוי פורטים. המדריך מסביר את סדר הקדימויות ושימוש ב-include לסביבות.

מה עושה Compose עם יותר מקובץ אחד

Docker Compose יכול לבנות פרויקט אחד ממספר קבצים. הוא קורא אותם לפי הסדר שבו הוא מקבל אותם וממזג אותם למודל יחיד, כך שקובץ מאוחר יותר דורס כל ערך שמתנגש. שני מנגנונים מבצעים זאת משורת הפקודה: קובץ override ש־Compose טוען בעצמו, והדגל -f שמעבירים ידנית. מנגנון שלישי קיים בתוך הקובץ עצמו, האלמנט include, והוא פועל אחרת משניהם.

המיזוג אינו דריסה פשוטה. מיפויים (mappings) מתמזגים מפתח אחר מפתח, רצפים (sequences) מתווספים זה לזה, וקבוצה קטנה של שדות מוחלפת בשלמותה. ההבדל הזה הוא המקור להפתעות, והרשימה ports היא זו שמכשילה כמעט את כולם.

כל מה שכתוב להלן מניח שימוש ב־Compose v2, התוסף docker compose ולא הסקריפט הישן docker-compose. הריצו docker compose version כדי לבדוק. אם עדיין לא כתבתם קובץ Compose, התחילו ב-מדריך היסודות של Docker Compose וחזרו לכאן.

קובץ ה-override ש-Compose טוען ללא הנחיה מפורשת

הריצו את docker compose up ללא דגל -f, ו-Compose יחפש את compose.yaml או docker-compose.yaml בספריית העבודה הנוכחית ולאחר מכן בספריות האב. אם קובץ override נמצא לצד קובץ הבסיס, 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 קורא אך ורק את הקובץ שצוין ומתעלם מה-override; זו התכונה שעליה מבוסס דפוס העבודה של פיתוח וייצור (dev/prod) בהמשך מדריך זה.

לכלל זה יש השלכות על שרתים. קובץ override שנותר בספריית הפריסה ייטען בכל הרצה של פקודת docker compose בסיסית מאותה ספרייה, כולל פקודות המופעלות על ידי cron job. כך קורה שסביבת ייצור מבצעת בטעות bind-mount לספריית קוד מקור שלא הייתה אמורה להגיע לשרת. הריצו docker compose config לאחר כל פריסה ובדקו את הפלט. כאשר הפריסה מתבצעת ללא השגחה, הבדיקה תועיל רק אם תקבלו התראה על כשל; זהו תפקידו של ערוץ דחיפה כמו שרת ntfy בניהול עצמי שאליו יכולים cron job או יחידת OnFailure של systemd לשלוח הודעות.

סדר הפעולות עם -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 מדווח ששירות מסוים אינו קיים. הסיכון עולה ב-stack שבו שדרוגים מורצים כפקודות חד-פעמיות, כמו שלב ה-database migration ב-מערכת תמיכה 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 של הפרויקט, מה שהופך אותו לחלק מה-checkout במקום לחלק מהיסטוריית ה-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, כיוון שהדריסה כותבת מחדש את השורה כולה.
  • מיפויים (Mappings) מתמזגים מפתח אחר מפתח. environment, labels, volumes ו-devices שומרים על כל מפתח משני הקבצים, כאשר הקובץ המאוחר יותר גובר בכל מפתח שקיים בשניהם. עבור environment ו-labels, המפתח הוא שם המשתנה או התווית. עבור volumes ו-devices, המפתח הוא הנתיב בתוך המכולה.
  • רצפים (Sequences) מתווספים לסוף. 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 לפני חוקי ה-firewall שלכם. המנגנון מוסבר ב-מדוע פורטים מפורסמים של Docker עוקפים את ufw.

ישנם שני פתרונות. הפתרון המפורש הוא התג !override, שמחליף את כל המאפיין ומדלג על חוקי המיזוג:

services:
  web:
    ports: !override
      - "127.0.0.1:8080:80"

!override דורש Compose בגרסה 2.24.4 או חדשה יותר. הפתרון הנייד אינו דורש תג כלל: השאירו את ports מחוץ לקובץ הבסיס לחלוטין והצהירו עליו רק בקבצים הספציפיים לסביבה. כשאין מה למזג, אין מה שידלוף. זהו הדפוס שבו נעשה שימוש בדוגמה המעשית להלן.

מחיקת ערך מקובץ הבסיס

!reset מסיר מאפיין ומחזיר אותו לערך ברירת המחדל שלו או ל-null. הפקודה מקבלת ערך ומתעלמת ממנו, לכן יש להזין ערך תקין וריק.

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

!reset דורש Compose בגרסה 2.24 ומעלה. השתמשו בו כאשר קובץ הבסיס אינו בבעלותכם ואינכם יכולים לערוך אותו, למשל במקרה של קובץ ספק שאתם מושכים. מחסנית (stack) רשמית של ספק היא דוגמה מובהקת לכך: קובץ ה-Compose שמאחורי סביבת עבודה עצמית של AFFiNE מגדיר ארבעה קונטיינרים שלא אתם כתבתם, ו-!reset מאפשר לכם לנקות מאפיין אחד באחד מהם מבלי לבצע fork לקובץ ולקחת על עצמכם את המשימה של מעקב אחר שינויים בו.

הכללה, עבור מחסניות המורכבות מחלקים

include מושך יישום Compose נוסף לתוך המודל שלכם. זהו רכיב ברמה העליונה (top-level), ולא flag.

include:
  - path: ../commons/compose.yaml

כל נתיב ב-include נטען כמודל יישום Compose עצמאי, עם ספריית פרויקט משלו, כך שנתיבים יחסיים בתוך אותו קובץ מתפענחים ביחס לספרייה של הקובץ עצמו. זהו ההבדל המהותי מ--f, וזו הסיבה ש-include הוא הכלי הנכון כאשר המקטע נמצא בתיקייה אחרת או במאגר (repository) אחר. זהו המבנה המקובל עבור מחסנית ספק (vendor stack) שלא אתם כתבתם: קובץ ה-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, שמעניק מראה של חנות השכרת סרטים משנות ה-90 לספריית Jellyfin: הקובץ שלו שומר על תג ה-image שלו ועל ה-env_file שלו, כך ששדרוג שלו לעולם לא מחייב נגיעה בקובץ שבו חיה מחסנית המדיה שלכם.

שמות משאבים כפולים בין הקובץ שלכם לבין קובץ כלול מדווחים כשגיאה במקום לעבור מיזוג שקט, וזה מכוון. כדי לשנות משהו שקובץ כלול מצהיר עליו, הכניסו את השינוי ל-compose.override.yaml: ה-override מוחל על המודל המורכב, כך שהוא יכול לגעת במשאבים כלולים מבלי להתנגש איתם. הרגל זה משתלם במיוחד במחסנית שקובץ ה-upstream שלה נכתב מחדש בכל גרסה (release), כמו שרתי התמונות מרובי-המכולות שנבחנו ב-PhotoPrism מול Immich, שבהם קישור ל-localhost או volume נוסף שייכים ל-override שלכם ולא לקובץ שהשדרוג הבא יחליף.

הגרסה המקוצרת: 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 לעולם. ראו קבצי env וסודות ב-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 מחליף את ערך ברירת המחדל של ה-image מכיוון שמדובר בערך יחיד. LOG_LEVEL מחליף את info מכיוון ש-environment מבצע מיזוג לפי מפתח. ה-bind mount ושני הפורטים החשופים הם תוספות בלבד, והפורט של מסד הנתונים מוגבל ל-localhost כדי שמחשב נייד ברשת משותפת לא יציע גישה ל-PostgreSQL לכל מי שנמצא בחדר.

לבסוף, compose.prod.yaml. שמו אינו כזה ש-Compose מחפש, לכן הוא לעולם לא ייטען בטעות.

services:
  app:
    ports:
      - "127.0.0.1:8000:3000"
    deploy:
      resources:
        limits:
          memory: 512M

בשרת ה-VPS עליכם לציין את שני הקבצים, ופעולת הציון הזו היא בדיוק מה שמונע את טעינת ה-override.

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 לא נקרא, ולכן פקודת הפיתוח, ה-bind mount של קוד המקור והפורט הציבורי 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 רגיל מדפיס כל secret מפוענח בטקסט גלוי. --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.

מכולות חוזרות עם שמות חדשים ונפח אחסון (volume) נראה ריק. שם הפרויקט השתנה, כיוון ששם הפרויקט נגזר מהתיקייה של הקובץ הראשון. הוסיפו name: ברמה העליונה לקובץ הבסיס והשמות יפסיקו להשתנות. הנפח הישן עדיין קיים תחת הקידומת הישנה, ו-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 עבור שכבות סביבה של ה-stack שלכם, וב-include עבור מקטע שמתוחזק במקום אחר. include דורש Compose v2.24.0 ומעלה.

כיצד מסירים ערך שנקבע בקובץ הבסיס?

השתמשו בתג !reset בגרסת Compose v2.24 ומעלה. כתבו ports: !reset [] או MY_VAR: !reset null בקובץ הדורס, והמאפיין יחזור לברירת המחדל שלו או ל-null. הערך שנותנים לתג נדרש אך מתעלמים ממנו. אם ברצונכם להחליף מאפיין במקום לנקות אותו, !override מבצע זאת, והוא דורש גרסה v2.24.4 ומעלה.