איך Compose ממזג כמה קובצי 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.
לכך יש גם צד מסוכן בשרת. קובץ עקיפה שנשאר בספריית הפריסה ייטען בכל פקודת docker compose ללא פרמטרים שמורצת מאותה ספרייה, כולל הפקודה שמריץ cron. כך מחסנית בסביבת ייצור עלולה לבצע bind mount לספריית מקור שלא הייתה מיועדת לפריסה. הריצו את docker compose config לאחר כל פריסה ובדקו את הפלט שהתקבל.
סדר הטעינה באמצעות -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 מדווח ששירות אינו קיים. הגדירו את הרשימה פעם אחת באמצעות משתנה הסביבה 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 ייצור לאחר מכן תיקייה ריקה בנתיב השגוי, וה-container יופעל בלי תוכן בתיקייה. מצב זה נראה כמו אובדן נתונים, אך הנתונים לא אבדו. העבירו את --project-directory כדי להגדיר בעצמכם את נתיב הבסיס, או השתמשו ב-include, שפותר כל קובץ ביחס לתיקייה שלו.
שם הפרויקט נגזר מאותה תיקיית בסיס. לכן שינוי הקובץ הראשון עשוי לשנות את שם הפרויקט. שינוי שם הפרויקט יוצר שמות חדשים ל-container ול-volume. ה-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 מזהה יציאה נוספת ולא קשורה, ולכן שומר את שתיהן.
מדוע היציאה עדיין מפורסמת לאחר ההחלפה
קובץ בסיס שמפרסם שירות בכל הממשקים:
services:
web:
image: nginx:1.27
ports:
- "8080:80"קובץ החלפה שנכתב כדי לקשור את השירות ל-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 בגרסה 2.24.4 ומעלה. הפתרון הנייד אינו דורש תגית כלל: השאירו את ports מחוץ לקובץ הבסיס לחלוטין והצהירו עליו רק בקבצים הספציפיים לסביבה. אם אין מה למזג, אין מה לדלוף. זו התבנית שבה נעשה שימוש בדוגמה המלאה להלן.
מחיקת ערך שהוגדר בקובץ הבסיס
!reset מסירה מאפיין ומחזירה אותו לערך ברירת המחדל שלו או ל־null. היא מקבלת ערך ומתעלמת ממנו, לכן יש לציין ערך תקין וריק.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset דורשת את Compose v2.24 ואילך. השתמשו בה כאשר אין לכם אפשרות לערוך את קובץ הבסיס, למשל כאשר מדובר במקטע של ספק שאתם כוללים.
include, עבור מחסניות המורכבות מחלקים
include טוען יישום Compose נוסף אל המודל שלכם. זהו רכיב ברמה העליונה, ולא דגל.
include:
- path: ../commons/compose.yamlכל נתיב ב־include נטען כמודל יישום Compose עצמאי, עם תיקיית פרויקט משלו. לכן נתיבים יחסיים בתוך הקובץ נפתרים ביחס לתיקייה של אותו קובץ. זהו ההבדל המהותי לעומת -f, וזו הסיבה ש־include הוא הכלי המתאים כאשר המקטע נמצא בתיקייה אחרת או במאגר אחר.
הצורה המלאה כוללת אפשרויות משנה.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath מקבל רשימה, והקבצים האלה מתמזגים זה בזה לפי הכללים הרגילים לפני שהתוצאה מצטרפת למודל שלכם. project_directory מגדיר את נתיב הבסיס המשמש לפתרון נתיבים יחסיים בקובץ שנכלל. env_file מעניק לקובץ שנכלל משתנים משלו לאינטרפולציה, וכך מונע ממקטע משותף לקרוא בשקט את .env של הפרויקט שלכם. include דורש Compose בגרסה 2.20.0 או חדשה יותר.
שמות משאבים כפולים בין הקובץ שלכם לבין קובץ שנכלל מדווחים כשגיאה במקום להתמזג בשקט, וזאת בכוונה. כדי לשנות משהו שהוגדר בקובץ שנכלל, הציבו את השינוי ב־compose.override.yaml: העקיפה מוחלת על המודל המורכב, ולכן יכולה לשנות משאבים שנכללו בלי להתנגש בהם.
בקיצור: 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 secrets לחלופות בטוחות יותר.
לאחר מכן, 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 ממזג לפי מפתח. ה-bind mount ושתי היציאות המפורסמות מתווספים בלבד, ויציאת מסד הנתונים נקשרת ל-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 psps אמור להציג את שני השירותים במצב פעיל, כאשר 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.
קראו את המודל הממוזג לפני הפריסה
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.
הקונטיינרים חוזרים עם שמות חדשים, ו-volume נראה ריק. שם הפרויקט השתנה, מכיוון ששם הפרויקט נגזר מתיקיית הקובץ הראשון. הוסיפו 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 ואילך.