ההבדלים בין env_file, environment ו-env ב-Docker Compose
מבולבלים בין env_file, environment וקובץ .env ב-Docker Compose? במדריך זה נסביר את סדר הקדימויות המדויק, נציג דוגמאות קוד ונלמד היכן באמת כדאי לשמור סיסמאות וסודות רגישים.
שלושת הדברים שאנשים מכנים קובץ env
ל-Docker Compose יש שלושה מנגנונים נפרדים בעלי שמות דומים ומבלבלים. הקובץ .env ממלא את ה-placeholders מסוג ${VARIABLE} בתוך ה-compose.yaml עצמו, לאחר ש-Compose קורא את ה-YAML ולפני שהוא מאמת או מריץ דבר מה. המאפיין env_file: טוען קובץ של זוגות מפתח/ערך לתוך סביבת העבודה של המכולה. המאפיין environment: מגדיר משתנים ישירות על המכולה, כשהם כתובים בתוך קובץ ה-compose. הם אינם ניתנים להחלפה, וכאשר שניים מהם מגדירים את אותו המפתח, המנצח נקבע לפי סדר קדימויות מתועד.
מדריך זה מציג כיצד כל אחד מהם עובד, מוכיח את סדר הקדימויות באמצעות פקודה שניתן להריץ, ולאחר מכן מכסה את החלק החשוב יותר: משתני סביבה ניתנים לקריאה על ידי כל מי שיכול להריץ docker inspect, ולכן סיסמאות אינן אמורות להופיע בהם. אם אתם חדשים לקובצי compose באופן כללי, התחילו ב-יסודות Docker Compose ב-VPS וחזרו לכאן לצורך הגדרות התצורה.
קובץ ה- מיועד לקובץ ה-compose, לא למכולה
צרו ספרייה והניחו בה שני קבצים.
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAGכעת בדקו מה Compose ניתח בפועל.
docker compose configהפלט מציג את image: alpine:3.20. מציין המקום הוסר, כיוון שהאינטרפולציה התרחשה בזמן ש-Compose טען את הקובץ. Compose מחפש את .env בספריית הפרויקט: הספרייה שצוינה עם --project-directory אם הגדרתם אותה, אחרת הספרייה של הקובץ הראשון שהועבר עם -f, וברירת המחדל היא ספריית העבודה הנוכחית שלכם. הוא מחליף את מצייני המקום מסוג ${NAME} שהוא מוצא בערכי ה-YAML. מפתחות לעולם אינם עוברים אינטרפולציה, ושימוש כפול ב-$$ מפיק סימן דולר מילולי במקום החלפה.
לאחר מכן הריצו את השירות.
docker compose run --rm demoprintenv ALPINE_TAG מסתיים עם סטטוס 1 ולא מדפיס דבר. המשתנה אינו קיים בתוך המכולה. זוהי אי-ההבנה הנפוצה ביותר: .env הגדיר את קובץ ה-compose, לא את התהליך. קובץ .env המכיל POSTGRES_PASSWORD=hunter2 אינו עושה דבר עבור מסד הנתונים שלכם, אלא אם חלק כלשהו בקובץ ה-compose מפנה אליו.
${NAME:-default} מספק ערך חלופי כאשר המשתנה אינו מוגדר או ריק. ${NAME:?message} גורם ל-Compose לסרב להפעיל את השירות ולהדפיס את ההודעה שלכם, וזו הבחירה הנכונה עבור ערך שאין לו ברירת מחדל בטוחה.
המאפיין env_file טוען משתנים לתוך המכולה
המאפיין env_file: מציין קובץ אחד או יותר שתוכנם הופך למשתני סביבה בתוך המכולה.
printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.envservices:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.envdocker compose run --rm demoפעולה זו מדפיסה from_env_file. פורמט הקובץ הוא שורות KEY=value פשוטות, משתנה אחד בכל שורה, כאשר # מסמן תחילת הערה. זהו אינו shell, אף ש-Compose מנתח אותו לפי כמה כללים דמויי shell. מרכאות מסביב לערך מוסרות, לכן KEY="value" ו-KEY=value יניבו את אותה התוצאה. ערכים במרכאות כפולות או ללא מרכאות עוברים אינטרפולציה של משתני ${...}, בעוד ערכים במרכאות בודדות משמשים כפי שהם (literal). קידומת export מקובלת אך אינה נדרשת. רווחים מסביב לסימן = מוסרים. יש לשים לב להערות מוטמעות: בערך ללא מרכאות, רווח ואחריו # מתחיל הערה, לכן יש להשתמש במרכאות עבור כל ערך המכיל #.
נתיב env_file חסר גורם לשגיאה ו-Compose עוצר. יש לסמן את הקובץ כאופציונלי אם ייתכן שהוא לא יהיה קיים באופן לגיטימי:
env_file:
- path: ./app.env
required: falseהגדרת משתני סביבה בתוך שורת הפקודה
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentקיימים שני תחבירים מקובלים: צורת המיפוי (mapping) לעיל, וצורת רשימה המשתמשת ב-- GREETING=from_environment. שניהם מתנהגים באופן זהה. שניהם חולקים טריק נוסף: מפתח ללא ערך מעביר את המשתנה מה-shell שבו הרצת את docker compose. בצורת הרשימה מדובר ב-- GREETING בודד, ובצורת המיפוי מדובר ב-GREETING: ללא כל תו אחרי הנקודתיים.
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoזה מדפיס את from_my_shell. אם תריץ זאת מבלי להגדיר את GREETING ב-shell, ה-Compose לא יגדיר דבר, ללא כל אזהרה. כדאי להכיר את הכשלים השקטים האלו בהעברת משתנים, כיוון ששירות שמתחיל עם משתנה סיסמה ריק לרוב יעלה בהצלחה, אך יישאר פרוץ לחלוטין.
מי מנצח
Docker מתעד את סדר הקדימויות, מהגבוה לנמוך: docker compose run -e בשורת הפקודה, לאחר מכן environment או env_file שהערך שלהם נשאב מה-shell או מקובץ env, לאחר מכן environment פשוט בקובץ ה-compose, לאחר מכן env_file, ולבסוף ההנחיה ENV שמוטמעת בתוך ה-image.
הגרסה המקוצרת לעבודה יומיומית: environment: גובר על env_file:, ו--e בשורת הפקודה גובר על שניהם. ניתן להוכיח זאת בקובץ אחד.
services:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.env
environment:
GREETING: from_environmentdocker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETINGהראשון מדפיס from_environment, לכן environment: דרס את הערך ב-app.env. השני מדפיס from_cli. שום דבר בקובץ ה-compose לא דורס את שורת הפקודה.
כאשר מכולה מתנהגת כאילו התצורה שלך לא הוחלה, אל תנחש. הפקודה docker compose config מדפיסה את הקובץ לאחר פתרון מלא, ו-docker compose config --environment מדפיסה את משתני ה-interpolation ש-Compose עובד איתם. רוב הדיווחים על "קובץ ה-env שלי מתעלמים ממנו" מתבררים כערך שהוגדר פעמיים בשתי רמות שונות.
מדוע משתני סביבה דולפים
הגדרת סיסמה בתוך environment: גורמת לכך שהיא נשמרת בתצורת המכולה על הדיסק, וחשופה לכל משתמש בקבוצת docker.
docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'הפלט מכיל את "DB_PASSWORD=hunter2" בטקסט גלוי. שלושה נתיבים נוספים חושפים את אותו הערך. הפקודה docker compose config מדפיסה אותו למסוף, וזו הדרך שבה הוא מגיע בסופו של דבר להדבקה בפורומים של תמיכה. תהליכי בת יורשים את המשתנה כברירת מחדל, אלא אם כן תהליך האב מפעיל אותם במכוון עם סביבה חלופית. תהליך שרץ תחת אותו משתמש כמו PID 1, או מחזיק ב-CAP_SYS_PTRACE, יכול לקרוא אותו מתוך /proc/1/environ. במכולה שבה הכל רץ כ-root, מדובר בכל תהליך. יישום שרץ כמשתמש ללא הרשאות מיוחדות בזמן ש-PID 1 רץ כ-root יקבל שגיאת הרשאות על קובץ זה, אם כי הוא עדיין מחזיק במשתנה בתוך הסביבה של עצמו. כמו כן, מנגנוני טיפול בקריסות של יישומים נוהגים להדפיס את כל הסביבה לתוך לוג או דוח שגיאות.
חברות בקבוצת docker שקולה למעשה להרשאות root על המארח, לכן אין מדובר בגבול הרשאות שאפשר להסתמך עליו. המדריך בנושא חשבונות משתמש בעלי הרשאות מינימליות ב-VPS מסביר מדוע כדאי להגביל את הקבוצה הזו בכל שרת משותף.
שימוש ב-Compose secrets לשמירת ערכים בקובץ
Compose תומך ב-secrets מבוססי קבצים. הערך ממופה (mounted) לתוך המכולה כקובץ, במקום שיוזרק למשתני הסביבה.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtה-secret ממופה בנתיב /run/secrets/db_password בתוך המכולה. השם המופיע אחרי הלוכסן הוא שם ה-secret כפי שהוגדר בבלוק ה-secrets: ברמה העליונה.
הסיומת _FILE היא מוסכמה המשמשת את ה-Docker Official Images, כולל postgres, mysql ו-mariadb. סקריפטים של entrypoint אלו בודקים את קיום הנתיב VARNAME_FILE, קוראים את הקובץ ומשתמשים בתוכנו. זו אינה תכונה מובנית של Docker, ולכן היא פועלת רק כאשר ה-image מיישם אותה. בדקו את התיעוד של ה-image לפני שתניחו ש-SOMETHING_FILE יכובד. יישומים שאינם תומכים בכך יכולים לרוב לקרוא את הקובץ בעצמם בעת העלייה, או שניתן להעביר את הנתיב ולתת ל-entrypoint שלכם לבצע את הקריאה.
בצעו אימות מתוך המכולה הרצה:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORDהפקודה הראשונה מדפיסה את הסיסמה. השנייה אינה מדפיסה דבר, כיוון שהערך מעולם לא הוזרק למשתני הסביבה. זוהי כל המטרה: docker inspect על מכולה זו מציג רק את הנתיב הבלתי מזיק.
הגנו על קובץ המקור במארח (host), שכן רמת הפרטיות של ה-secret היא כרמת הפרטיות של הקובץ שמאחוריו:
chmod 600 db_password.txtדרך האמצע הפרגמטית בשרת VPS
דימויים (images) רבים של אירוח עצמי אינם תומכים במשתני _FILE, לכן משתני סביבה הם הדרך היחידה להזנת נתונים. בשרת VPS עם מנהל מערכת יחיד, המטרה הריאלית היא למנוע מהערכים להישמר בקובץ שקריא לכל משתמש בספריית הפרויקט, ולהרחיק אותם מ-git.
sudo install -o root -g root -m 600 /dev/null /etc/myapp/app.env
sudo nano /etc/myapp/app.env env_file:
- /etc/myapp/app.envהפקודה install -m 600 יוצרת את הקובץ עם הרשאות מוגדרות מראש, כך שאין פרק זמן שבו הקובץ חשוף לקריאה על ידי כולם. הבעלות על הקובץ היא של root, לכן משתמש שאינו root בשרת לא יכול לקרוא אותו, אם כי כל מי שיכול להריץ docker עדיין יכול לקרוא את הערך מתוך המכולה. הוסיפו את *.env ואת .env לקובץ .gitignore, ובצעו commit לקובץ app.env.example המכיל את שמות המפתחות עם ערכים ריקים בלבד. סיסמה שבוצעה לה commit היא סיסמה שיש להחליף (rotate).
החלפת ערך מחייבת אתחול של השירות. משתני סביבה נקראים פעם אחת בלבד בעת עליית תהליך המכולה, לכן עריכת הקובץ לא תשנה דבר עד להרצת docker compose up -d --force-recreate db. זהו אותו דפוס שבו נעשה שימוש במדריך n8n מאחורי HTTPS בשרת VPS, שבו מפתח ההצפנה נמצא מחוץ לקובץ ה-compose.
פיצול הגדרות לפי סביבה
כברירת מחדל, Compose קורא את .env מתיקיית הפרויקט. ניתן להפנות אותו למיקום אחר באמצעות --env-file.
docker compose --env-file .env.staging configקבצים מרובים נקראים לפי סדר הופעתם, כאשר קבצים מאוחרים יותר דורסים את הקודמים. שמרו את הגדרות ברירת המחדל שאינן רגישות בקובץ המנוהל ב-version control, ואת הסודות בקובץ שאינו עוזב את השרת לעולם. אותו עיקרון חל גם על env_file:, שבו הקובץ האחרון ברשימה הוא הקובע במקרה של מפתח כפול.
FAQ
מדוע קובץ ה-.env שלי מתעלם בתוך המכולה?
הוא אינו מתעלם. הקובץ .env מחליף מצייני מיקום מסוג ${NAME} בקובץ ה-compose, והוא יכול גם להגדיר משתנים של Compose עצמו, כגון COMPOSE_PROJECT_NAME או COMPOSE_FILE. הוא לעולם אינו מגדיר משתנים בתוך מכולה. כדי להעביר את הערך לתוך המכולה, יש להפנות אליו באמצעות environment: { KEY: "${NAME}" }, או להשתמש ב-env_file: ./that-file.env במקום זאת.
האם environment דורס את env_file, או להפך?
environment: גובר. סדר הקדימויות המתועד של Docker מציב את המאפיין environment מעל המאפיין env_file, ושניהם נמצאים מתחת ל-docker compose run -e בשורת הפקודה. אם מפתח מוגדר בשני המקומות, הערך ב-env_file לא יהיה בשימוש, ללא התראה.
כיצד אוכל לראות את הערך הסופי ש-Compose ישתמש בו?
הריצו את docker compose config כדי להדפיס את קובץ ה-compose לאחר שכל ה-interpolation הוחל עליו. עבור מכולה שכבר רצה, docker inspect <container> --format '{{json .Config.Env}}' מציג בדיוק מה התהליך שלה קיבל.
האם secrets ב-Compose מוצפנים?
לא. secret המבוסס על קובץ ממופה לתוך המכולה כקובץ טקסט פשוט ב-/run/secrets/<name>, וקובץ המקור נשמר בדיסק המארח ללא הצפנה. היתרון הוא בטווח ההשפעה (scope), לא בהצפנה: הערך נשאר מחוץ לסביבת המכולה, מחוץ לפלט של docker inspect, ומחוץ ל-crash dumps שמדפיסים את משתני הסביבה.
האם ניתן להשתמש במירכאות וברווחים בקובץ env?
כן. Compose מסיר את המירכאות העוטפות, לכן KEY="value with spaces" ו-KEY=value with spaces שניהם יתנו value with spaces. השתמשו במירכאות בודדות כאשר הערך מכיל $ שאין להחיל עליו interpolation, ושימו במירכאות כל ערך המכיל #, כיוון שללא מירכאות התו הזה מתחיל הערה. רווחים סביב ה-= מוסרים, לכן KEY = value עדיין יגדיר את KEY.