رفتن به محتوا
LoopX

متغیرها و فایل env در Compose

توی این درس یاد می‌گیری تنظیمات و رازها را از فایل Compose جدا کنی: با فایل .env و جایگزینی ${VAR} (همراه مقدار پیش‌فرض و اجباری)، با env_file برای ریختن متغیرها داخل کانتینر، و بفهمی اولویت منبع‌ها چیست. بعد با profiles سرویس‌های اختیاری (مثل ابزار دیباگ) بسازی و با فایل compose.override.yaml یک پروژه را در دو حالت dev و prod اجرا کنی. دستورهای اصلی: docker compose config و docker compose --profile dev up.

مسئله: یک فایل، چند محیط

Section titled “مسئله: یک فایل، چند محیط”

اپ تو روی لپ‌تاپ با پورت ۸۰۰۰ و پسورد ساده اجرا می‌شود، ولی روی سرور باید پورت ۸۰ و پسورد واقعی داشته باشد. اگر این مقدارها را داخل compose.yaml بنویسی، یا باید برای هر محیط یک نسخه‌ی جدا نگه داری (که بعد از چند هفته با هم نمی‌خوانند)، یا رازهایت داخل Git می‌روند. راه درست: ساختار در compose.yaml، مقدارها بیرون از آن.

compose.yaml مثل یک فرم چاپی است که جای بعضی خانه‌هایش خالی است (${PORT})، و .env برگه‌ی پر کردن آن خانه‌ها. فرم را همه‌ی تیم دارند (در Git)، برگه‌ی پرشده مال هر کس است. اگر خانه‌ی اجباری خالی بماند، فرم قبول نمی‌شود (خطا).

دو مفهوم که اغلب قاطی می‌شوند

Section titled “دو مفهوم که اغلب قاطی می‌شوند”
سمت چپ: .env فقط برای جایگزینی ${...} داخل خود compose.yaml است. سمت راست: env_file و environment متغیرها را داخل کانتینر می‌ریزند.

مثال ۱: .env و جایگزینی ${VAR}

Section titled “مثال ۱: .env و جایگزینی ${VAR}”
envdemo/.env
WEB_PORT=8385
GREETING=سلام از env
envdemo/compose.yaml
name: lxenv
services:
web:
image: nginx:alpine
ports:
- "${WEB_PORT:-8080}:80"
environment:
GREETING: ${GREETING}
MODE: ${MODE:-dev}

بخش ${WEB_PORT:-8080} یعنی «مقدار WEB_PORT؛ اگر تعریف نشده بود 8080». docker compose config فایل را بعد از جایگزینی نشان می‌دهد (بدون اجرای چیزی):

Terminal window
cd envdemo
docker compose config | grep -E 'published|GREETING|MODE'
خروجی
GREETING: سلام از env
MODE: dev
published: "8385"

پورت از .env آمد (۸۳۸۵)، GREETING از .env، و MODE چون در .env نبود مقدار پیش‌فرض dev را گرفت. config مهم‌ترین ابزار عیب‌یابی این بخش است: هر وقت مطمئن نیستی چه مقداری جایگزین شده، config بزن.

⚡ بررسی سریع

${PORT:-3000} یعنی چه؟

مثال ۲: متغیر اجباری با :?

Section titled “مثال ۲: متغیر اجباری با :?”
Terminal window
mkdir -p req && cat > req/compose.yaml <<'LXEOF'
services:
db:
image: alpine
environment:
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD را در فایل .env تعریف کن}
LXEOF
cd req && docker compose config 2>&1 | head -2
echo "--- با تعریف متغیر:"
DB_PASSWORD=secret123 docker compose config | grep DB_PASSWORD
خروجی
error while interpolating services.db.environment.DB_PASSWORD: required variable DB_PASSWORD is missing a value: DB_PASSWORD را در فایل .env تعریف کن
--- با تعریف متغیر:
DB_PASSWORD: secret123

بدون DB_PASSWORD همان لحظه‌ی config (یا up) با پیام خودت متوقف می‌شود. به‌جای اینکه کانتینر با پسورد خالی بالا بیاید و بعد از ساعت‌ها مشکل پیدا شود، زود و بلند خطا می‌گیری. برای رازها همین :? را بگذار.

envdemo/app.env
APP_NAME=shop
APP_COLOR=blue
envdemo/compose.apps.yaml
name: lxenv2
services:
show:
image: alpine
env_file:
- app.env
environment:
APP_COLOR: red
command: sh -c 'printenv | grep ^APP_ | sort'
Terminal window
cd envdemo
docker compose -f compose.apps.yaml run --rm show 2>&1 | grep -vE 'Container|Network'
خروجی
APP_COLOR=red
APP_NAME=shop

app.env دو متغیر داد، ولی APP_COLOR در environment: دوباره آمده و برنده شد (red، نه blue). قاعده: environment: از env_file: قوی‌تر است. env_file وقتی عالی است که ده‌ها متغیر داری و نمی‌خواهی فایل Compose شلوغ شود.

برای جایگزینی ${VAR} در خود Compose، ترتیب از قوی به ضعیف این است: متغیر شل ← فایل .env ← مقدار پیش‌فرض داخل فایل. ببین:

Terminal window
cd envdemo
echo "فقط .env: $(docker compose config | grep 'GREETING:')"
echo "با متغیر شل: $(GREETING='از شل' docker compose config | grep 'GREETING:')"
echo "با --env-file: $(printf 'GREETING=از فایل دیگر\n' > /var/tmp/lx-other.env; docker compose --env-file /var/tmp/lx-other.env config | grep 'GREETING:')"
rm -f /var/tmp/lx-other.env
خروجی
فقط .env: GREETING: سلام از env
با متغیر شل: GREETING: از شل
با --env-file: GREETING: از فایل دیگر

متغیر شل از .env قوی‌تر است، و با --env-file می‌توانی به‌جای .env فایل دیگری بدهی (مثلاً prod.env). این برای CI و سرور مفید است: همان compose.yaml، مقدار از بیرون.

مثال ۵: profiles (پروفایل‌ها)، سرویس‌های اختیاری

Section titled “مثال ۵: profiles (پروفایل‌ها)، سرویس‌های اختیاری”
envdemo/compose.tools.yaml
name: lxenv3
services:
web:
image: nginx:alpine
debug:
image: alpine
command: sleep 300
profiles: [dev]

سرویسی که profiles دارد فقط وقتی پروفایلش فعال شود بالا می‌آید:

Terminal window
cd envdemo
echo "# بدون پروفایل:"
docker compose -f compose.tools.yaml up -d 2>&1 | grep -E "Started" | sed -E 's/^ +//'
docker compose -f compose.tools.yaml ps --format '{{.Service}}'
docker compose -f compose.tools.yaml down >/dev/null 2>&1
echo "# با --profile dev:"
docker compose -f compose.tools.yaml --profile dev up -d 2>&1 | grep -E "Started" | sed -E 's/^ +//' | sort
docker compose -f compose.tools.yaml --profile dev ps --format '{{.Service}}' | sort
docker compose -f compose.tools.yaml --profile dev down >/dev/null 2>&1
خروجی
# بدون پروفایل:
Container lxenv3-web-1 Started
web
# با --profile dev:
Container lxenv3-debug-1 Started
Container lxenv3-web-1 Started
debug
web

بدون پروفایل فقط web اجرا شد؛ با --profile dev سرویس debug هم آمد. به‌جای پروفایل می‌توانی متغیر COMPOSE_PROFILES=dev را هم بگذاری (مثلاً در .env).

مثال ۶: فایل override برای dev و prod

Section titled “مثال ۶: فایل override برای dev و prod”

Compose به‌طور خودکار compose.yaml را با compose.override.yaml (اگر باشد) ادغام می‌کند. فایل پایه‌ی مشترک:

ovr/compose.yaml
name: lxovr
services:
web:
image: nginx:alpine
restart: unless-stopped

تنظیمات خاص dev (به‌صورت خودکار خوانده می‌شود):

ovr/compose.override.yaml
services:
web:
ports:
- "8386:80"
environment:
DEBUG: "1"

تنظیمات prod (فقط وقتی صریحاً با -f بدهی):

ovr/compose.prod.yaml
services:
web:
ports:
- "8387:80"
environment:
DEBUG: "0"
Terminal window
cd ovr
echo "# dev (خودکار: compose.yaml + compose.override.yaml)"
docker compose config | grep -E "published|DEBUG|restart"
echo "# prod (صریح: -f compose.yaml -f compose.prod.yaml)"
docker compose -f compose.yaml -f compose.prod.yaml config | grep -E "published|DEBUG|restart"
خروجی
# dev (خودکار: compose.yaml + compose.override.yaml)
DEBUG: "1"
published: "8386"
restart: unless-stopped
# prod (صریح: -f compose.yaml -f compose.prod.yaml)
DEBUG: "0"
published: "8387"
restart: unless-stopped

در dev پورت ۸۳۸۶ و DEBUG=1، در prod پورت ۸۳۸۷ و DEBUG=0؛ restart از فایل پایه به هر دو رسید. با -f دادن، override خودکار خوانده نمی‌شود، و فایل‌های بعدی روی قبلی‌ها ادغام می‌شوند.

ادغام فایل‌ها چطور کار می‌کند؟ Compose فایل‌ها را به ترتیب می‌خواند و روی هم می‌ریزد:

  • مقدار تکی (مثل image، command): فایل بعدی جایگزین می‌شود.
  • map (مثل environment به‌شکل کلید/مقدار): کلیدها ادغام می‌شوند؛ کلید مشترک مقدار فایل بعدی را می‌گیرد.
  • list (مثل ports، volumes): ردیف‌ها به هم اضافه می‌شوند (نه جایگزین). برای همین اگر override پورت بدهد و پایه هم پورت داشته باشد، هر دو باز می‌شوند. اگر می‌خواهی جایگزین کنی، پایه را بدون ports بنویس (مثل مثال بالا).
لایه‌ها از ضعیف به قوی: مقدار پیش‌فرض ← .env ← متغیر شل. و برای فایل‌ها: compose.yaml ← override یا -f های بعدی.

نکته‌ی بسیار مهم: .env فقط برای جایگزینی داخل فایل Compose است. اگر .env شامل DB_PASSWORD باشد ولی در سرویس نه environment: و نه env_file: گذاشته باشی، آن متغیر داخل کانتینر نیست. (درس‌آموز: docker compose run --rm svc printenv را بزن و ببین.)

روش کجا خوانده می‌شود چه می‌کند
.env کنار compose.yaml جایگزینی ${VAR} در فایل Compose
--env-file f خط فرمان جایگزین .env پیش‌فرض
env_file: داخل سرویس متغیرها را داخل کانتینر می‌ریزد
environment: داخل سرویس متغیر مستقیم داخل کانتینر (قوی‌تر از env_file)
متغیر شل محیط ترمینال قوی‌ترین منبع برای ${VAR}
سینتکس معنی
${VAR} مقدار VAR (خالی اگر نبود، با هشدار)
${VAR:-x} اگر تعریف نشده یا خالی، x
${VAR-x} فقط اگر تعریف نشده، x
${VAR:?msg} اگر نبود، خطا با پیام msg
$$ علامت دلار واقعی
Terminal window
mkdir -p m1 && printf 'services:\n a:\n image: alpine\n environment:\n X: ${NOPE}\n' > m1/compose.yaml
cd m1 && docker compose config 2>&1 | head -1 | sed -E 's/^time="[^"]*" //'
خروجی
level=warning msg="The \"NOPE\" variable is not set. Defaulting to a blank string."

بدون مقدار پیش‌فرض، خالی جایگزین می‌شود و فقط یک هشدار می‌بینی (که در لاگ‌های طولانی گم می‌شود). راه‌حل: برای مقدار لازم :? بگذار.

۲) انتظار داشتن متغیر .env داخل کانتینر

Section titled “۲) انتظار داشتن متغیر .env داخل کانتینر”
Terminal window
mkdir -p m2 && printf 'SECRET=abc\n' > m2/.env && printf 'services:\n a:\n image: alpine\n command: sh -c "echo SECRET=[$$SECRET]"\n' > m2/compose.yaml
cd m2 && docker compose run --rm a 2>&1 | grep -vE 'Container|Network'
خروجی
SECRET=[]

SECRET در .env هست ولی در سرویس نیامده، پس داخل کانتینر خالی است. راه‌حل: در environment: یا env_file: تعریفش کن.

۳) فراموش کردن $$ برای متغیرهای داخل کانتینر

Section titled “۳) فراموش کردن $$ برای متغیرهای داخل کانتینر”

در مثال بالا $$SECRET نوشتیم تا Compose آن را جایگزین نکند و به شل داخل کانتینر برسد. با یک $ تنها، Compose سعی می‌کند خودش جایگزین کند. راه‌حل: برای $ واقعی $$ بنویس.

اگر .env را commit کردی، حتی بعد از حذف در تاریخچه می‌ماند. راه‌حل: از اول .gitignore، و .env.example برای راهنما؛ اگر رمز لو رفت، رمز را عوض کن (حذف از Git کافی نیست).

۵) ports در override که به پایه اضافه می‌شود

Section titled “۵) ports در override که به پایه اضافه می‌شود”

اگر compose.yaml پورت ۸۰:۸۰ و override پورت ۸۰۸۰:۸۰ بدهد، هر دو باز می‌شوند (list ها اضافه می‌شوند). راه‌حل: پورت را فقط در یک فایل بگذار، یا پایه را بدون ports بنویس.

✎ تمرینآسان

با .env و ${PORT:-9000} پورتی بساز که بدون .env برابر ۹۰۰۰ باشد. با docker compose config هر دو حالت را نشان بده.

دیدن جواب
Terminal window
mkdir -p e1 && printf 'services:\n web:\n image: nginx:alpine\n ports:\n - "${PORT:-9000}:80"\n' > e1/compose.yaml
cd e1
echo "بدون .env: $(docker compose config | grep published)"
printf 'PORT=9100\n' > .env
echo "با .env: $(docker compose config | grep published)"
خروجی
بدون .env: published: "9000"
با .env: published: "9100"
✎ تمرینمتوسط

تمرین اصلی: دو حالت dev و prod برای یک پروژه بساز. dev پورت ۸۳۸۸ و LOG_LEVEL=debug، prod پورت ۸۳۸۹ و LOG_LEVEL=warn داشته باشد؛ با compose.override.yaml برای dev و compose.prod.yaml برای prod. با config مقدارها را نشان بده و سپس prod را واقعاً اجرا و با curl تست کن.

دیدن جواب
Terminal window
mkdir -p e2 && cd e2
printf 'name: lxe2\nservices:\n web:\n image: nginx:alpine\n' > compose.yaml
printf 'services:\n web:\n ports: ["8388:80"]\n environment:\n LOG_LEVEL: debug\n' > compose.override.yaml
printf 'services:\n web:\n ports: ["8389:80"]\n environment:\n LOG_LEVEL: warn\n' > compose.prod.yaml
echo "dev : $(docker compose config | grep -E 'published|LOG_LEVEL' | tr -s ' ' | tr '\n' ' ')"
echo "prod: $(docker compose -f compose.yaml -f compose.prod.yaml config | grep -E 'published|LOG_LEVEL' | tr -s ' ' | tr '\n' ' ')"
docker compose -f compose.yaml -f compose.prod.yaml up -d >/dev/null 2>&1; sleep 2
echo "curl prod: HTTP $(curl -s -o /dev/null -w '%{http_code}' localhost:8389)"
docker compose -f compose.yaml -f compose.prod.yaml down >/dev/null 2>&1
خروجی
dev : LOG_LEVEL: debug published: "8388"
prod: LOG_LEVEL: warn published: "8389"
curl prod: HTTP 200
✎ تمرینسخت

یک compose.yaml بنویس که سرویس app (alpine) متغیر TOKEN را اجباری از .env بگیرد (با :?)، یک سرویس tools در پروفایل dev داشته باشد و app هم printenv TOKEN را اجرا کند. نشان بده: ۱) بدون .env خطا می‌دهد؛ ۲) با .env اجرا می‌شود؛ ۳) tools فقط با --profile dev در config --services می‌آید.

دیدن جواب
Terminal window
mkdir -p e3 && cd e3
cat > compose.yaml <<'LXEOF'
name: lxe3
services:
app:
image: alpine
environment:
TOKEN: ${TOKEN:?TOKEN لازم است}
command: printenv TOKEN
tools:
image: alpine
command: sleep 5
profiles: [dev]
LXEOF
echo "۱) $(docker compose config 2>&1 | head -1)"
printf 'TOKEN=abc123\n' > .env
echo "۲) $(docker compose run --rm app 2>&1 | grep -vE 'Container|Network')"
echo "۳) بدون پروفایل: $(docker compose config --services | tr '\n' ' ')"
echo " با dev: $(docker compose --profile dev config --services | sort | tr '\n' ' ')"
خروجی
۱) error while interpolating services.app.environment.TOKEN: required variable TOKEN is missing a value: TOKEN لازم است
۲) abc123
۳) بدون پروفایل: app
با dev: app tools
؟ آزمونک
  1. فایل .env چه می‌کند؟

  2. اگر در env_file و environment هر دو APP_COLOR بیاید، کدام برنده است؟

  3. کدام سینتکس اگر متغیر نبود خطا می‌دهد؟

  4. سرویس با profiles: [dev] چه زمانی بالا می‌آید؟

  5. با docker compose -f a.yaml -f b.yaml فایل compose.override.yaml…

  6. در ادغام فایل‌ها ports (یک list) چه می‌شود؟

  • ساختار در compose.yaml، مقدارها در .env (و رازها هرگز در Git).
  • ${VAR:-پیش‌فرض} و ${VAR:?پیام} برای مقدار اختیاری و اجباری؛ docker compose config نتیجه‌ی نهایی را نشان می‌دهد.
  • .env فقط جایگزینی داخل Compose است؛ برای داخل کانتینر environment: یا env_file: لازم است (و environment قوی‌تر).
  • اولویت ${VAR}: متغیر شل ← .env (یا --env-file) ← پیش‌فرض.
  • profiles سرویس‌های اختیاری؛ compose.override.yaml برای dev (خودکار) و -f برای prod.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker compose configنمایش فایل بعد از جایگزینی و ادغام
docker compose --env-file prod.env configاستفاده از فایل env دیگر
docker compose --profile dev up -dاجرا با پروفایل dev
docker compose -f a.yaml -f b.yaml upادغام صریح فایل‌ها
${VAR:-x}مقدار پیش‌فرض
${VAR:?msg}اجباری، با پیام خطا
env_file: [app.env]ریختن متغیرها داخل کانتینر
docker compose config --servicesفهرست سرویس‌های فعال