ההבדל בין .env ל-env_file ו-environment ב-Docker Compose
מבולבלים בין .env, env_file ו-environment ב-Docker Compose? במדריך זה תלמדו מהו סדר הקדימויות המדויק, מתי להשתמש בכל אחד מהם ומדוע אסור לשמור סיסמאות במשתני סביבה.
שלושת הדברים שאנשים מכנים קובץ env
ל-Docker Compose יש שלושה מנגנונים נפרדים בעלי שמות דומים ומבלבלים. הקובץ .env ממלא מצייני מיקום מסוג ${VARIABLE} בתוך compose.yaml עצמו, עוד לפני ש-Compose מנתח את הקובץ. המאפיין env_file: טוען קובץ של זוגות מפתח/ערך לתוך סביבת הקונטיינר. המאפיין environment: מגדיר משתנים ישירות על הקונטיינר, כשהם כתובים בתוך קובץ ה-compose. הם אינם ניתנים להחלפה, וכאשר שניים מהם מגדירים את אותו המפתח, המנצח נקבע לפי סדר קדימויות מתועד.
מדריך זה מציג כיצד כל אחד מהם פועל, מוכיח את סדר הקדימויות באמצעות פקודה שניתן להריץ, ולאחר מכן מכסה את החלק החשוב יותר: משתני סביבה ניתנים לקריאה על ידי כל מי שיכול להריץ docker inspect, לכן סיסמאות אינן שייכות אליהם. אם אתם חדשים לקבצי compose באופן כללי, התחילו ב-יסודות Docker Compose ב-VPS וחזרו לכאן לצורך הגדרות.
הקובץ .env מיועד לקובץ ה-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 מחפש את .env בספריית הפרויקט, שהיא הספרייה המכילה את קובץ ה-compose, ומחליף כל ${NAME} שהוא מוצא.
לאחר מכן הריצו את השירות.
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. במרבית המקרים, מרכאות נשמרות כחלק מהערך, ואין צורך בקידומות export. אין להוסיף רווחים סביב סימן ה-=, כיוון ש-KEY = value יוצר משתנה ששמו המילולי הוא KEY עם רווח מוביל בערכו.
נתיב env_file חסר מהווה שגיאה ו-Compose נעצר. יש לסמן את הקובץ כאופציונלי אם ייתכן שהוא לא יהיה קיים באופן תקין:
env_file:
- path: ./app.env
required: falseהגדרת משתני סביבה בשורה אחת
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentקיימים שני תחבירים מקובלים: צורת המיפוי שלעיל וצורת רשימה המשתמשת ב-- GREETING=from_environment. שניהם מתנהגים באופן זהה. לצורת הרשימה יש טריק נוסף: מפתח חשוף ללא ערך מעביר את המשתנה מה-shell שבו הרצת את docker compose.
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 לא דורס את שורת הפקודה.
כאשר container מתנהג כאילו ההגדרות שלך לא הוחלו, אל תנחש. הפקודה 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 מדפיס אותו למסוף, וכך הוא מגיע בסופו של דבר להדבקה בפורומי תמיכה. כל תהליך בתוך הקונטיינר יכול לקרוא את /proc/1/environ וכל תהליך בן יורש את המשתנה. כמו כן, מנגנוני טיפול בקריסות של יישומים נוהגים להשליך את כל הסביבה לקובץ לוג או לדוח שגיאות.
חברות בקבוצת docker שקולה להרשאות root על המארח, לכן אין מדובר בגבול הרשאות שניתן להסתמך עליו. המדריך בנושא חשבונות משתמש בעלי הרשאות מינימליות ב-VPS מסביר מדוע כדאי להגביל את הקבוצה הזו בכל שרת משותף.
שמירת סודות ב-Compose באמצעות קובץ
Compose תומך בסודות מבוססי-קובץ. הערך מעוגן (mounted) לתוך הקונטיינר כקובץ, במקום להיות מוזרק למשתני הסביבה.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtהסוד מעוגן בנתיב /run/secrets/db_password בתוך הקונטיינר. השם המופיע לאחר הלוכסן הוא שם הסוד מתוך בלוק ה-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), כיוון שהסוד מאובטח רק במידה שבה הקובץ שמאחוריו מאובטח:
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.envinstall -m 600 יוצר את הקובץ עם הרשאות מוגדרות מראש, כך שאין פרק זמן שבו הקובץ נגיש לקריאה על ידי כולם. הבעלות על הקובץ היא של root, לכן משתמש שאינו root על השרת אינו יכול לקרוא אותו, אם כי כל מי שיכול להריץ docker עדיין יכול לקרוא את הערך מתוך ה-container. יש להוסיף את *.env ואת .env ל-.gitignore ולבצע commit ל-app.env.example המכיל את שמות המפתחות עם ערכים ריקים במקום זאת. סיסמה שבוצעה לה commit היא סיסמה שחובה להחליף (rotate).
החלפת ערך משמעותה הפעלה מחדש של השירות. משתני סביבה נקראים פעם אחת בעת הפעלת תהליך ה-container, לכן עריכת הקובץ לא משנה דבר עד להרצת docker compose up -d --force-recreate db. זהו אותו דפוס המשמש במדריך n8n מאחורי HTTPS ב-VPS, שבו מפתח ההצפנה נמצא מחוץ לקובץ ה-compose.
פיצול הגדרות לפי סביבה
כברירת מחדל, Compose קורא את .env מתוך ספריית הפרויקט. ניתן להפנות אותו למיקום אחר באמצעות --env-file.
docker compose --env-file .env.staging configקבצים מרובים נקראים לפי סדר הופעתם, כאשר קבצים מאוחרים יותר דורסים הגדרות מקבצים מוקדמים. שמרו הגדרות ברירת מחדל שאינן סודיות בקובץ המנוהל ב-commit, ואת הסודות בקובץ שאינו עוזב את השרת. אותו כלל חל על env_file:, שבו הקובץ האחרון ברשימה הוא הקובע במקרה של מפתח כפול.
FAQ
מדוע הקובץ .env שלי אינו נלקח בחשבון בתוך הקונטיינר?
הוא אינו מתעלם ממנו. הקובץ .env מבצע רק החלפה של מצייני מיקום מסוג ${NAME} בתוך קובץ ה-compose. הוא לעולם אינו מגדיר משתנים בתוך קונטיינר. כדי להעביר את הערך לתוך הקונטיינר, יש להפנות אליו באמצעות 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 לאחר שכל ההחלפות בוצעו במלואן. עבור קונטיינר שכבר רץ, docker inspect <container> --format '{{json .Config.Env}}' מציג בדיוק מה התהליך שלו קיבל.
האם סודות (secrets) ב-Compose מוצפנים?
לא. סוד המבוסס על קובץ ממופה לתוך הקונטיינר כקובץ טקסט רגיל ב-/run/secrets/<name>, וקובץ המקור נשמר על הדיסק של המארח ללא הצפנה. היתרון הוא בהיקף החשיפה, לא בהצפנה: הערך נשאר מחוץ לסביבת הקונטיינר, מחוץ לפלט של docker inspect, ומחוץ לקבצי דאמפ של קריסה המדפיסים את משתני הסביבה.
האם ניתן להשתמש במירכאות וברווחים בקובץ env?
השתמשו ב-KEY=value with spaces והימנעו משימוש במירכאות. Compose מתייחס לכל המשך השורה כאל הערך עצמו, לכן מירכאות בדרך כלל הופכות לתווים מילוליים בתוך הערך. לעולם אל תוסיפו רווחים סביב ה-=, כיוון שאז המפתח יכלול רווח בסופו ושום דבר לא יתאים לו.