توی این درس یاد میگیری تنظیمات و رازها را از فایل 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، مقدارها بیرون از آن.
تشبیه: فرم و جاهای خالی
Section titled “تشبیه: فرم و جاهای خالی”compose.yaml مثل یک فرم چاپی است که جای بعضی خانههایش خالی است (${PORT})، و .env برگهی پر کردن آن خانهها. فرم را همهی تیم دارند (در Git)، برگهی پرشده مال هر کس است. اگر خانهی اجباری خالی بماند، فرم قبول نمیشود (خطا).
دو مفهوم که اغلب قاطی میشوند
Section titled “دو مفهوم که اغلب قاطی میشوند”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: .env و جایگزینی ${VAR}
Section titled “مثال ۱: .env و جایگزینی ${VAR}”WEB_PORT=8385GREETING=سلام از envname: 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 فایل را بعد از جایگزینی نشان میدهد (بدون اجرای چیزی):
cd envdemodocker 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 “مثال ۲: متغیر اجباری با :?”mkdir -p req && cat > req/compose.yaml <<'LXEOF'services: db: image: alpine environment: DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD را در فایل .env تعریف کن}LXEOFcd req && docker compose config 2>&1 | head -2echo "--- با تعریف متغیر:"DB_PASSWORD=secret123 docker compose config | grep DB_PASSWORDerror 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) با پیام خودت متوقف میشود. بهجای اینکه کانتینر با پسورد خالی بالا بیاید و بعد از ساعتها مشکل پیدا شود، زود و بلند خطا میگیری. برای رازها همین :? را بگذار.
مثال ۳: env_file و environment
Section titled “مثال ۳: env_file و environment”APP_NAME=shopAPP_COLOR=bluename: lxenv2
services: show: image: alpine env_file: - app.env environment: APP_COLOR: red command: sh -c 'printenv | grep ^APP_ | sort'cd envdemodocker compose -f compose.apps.yaml run --rm show 2>&1 | grep -vE 'Container|Network'APP_COLOR=redAPP_NAME=shopapp.env دو متغیر داد، ولی APP_COLOR در environment: دوباره آمده و برنده شد (red، نه blue). قاعده: environment: از env_file: قویتر است. env_file وقتی عالی است که دهها متغیر داری و نمیخواهی فایل Compose شلوغ شود.
مثال ۴: اولویت منبعها
Section titled “مثال ۴: اولویت منبعها”برای جایگزینی ${VAR} در خود Compose، ترتیب از قوی به ضعیف این است: متغیر شل ← فایل .env ← مقدار پیشفرض داخل فایل. ببین:
cd envdemoecho "فقط .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 (پروفایلها)، سرویسهای اختیاری”name: lxenv3
services: web: image: nginx:alpine debug: image: alpine command: sleep 300 profiles: [dev]سرویسی که profiles دارد فقط وقتی پروفایلش فعال شود بالا میآید:
cd envdemoecho "# بدون پروفایل:"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>&1echo "# با --profile dev:"docker compose -f compose.tools.yaml --profile dev up -d 2>&1 | grep -E "Started" | sed -E 's/^ +//' | sortdocker compose -f compose.tools.yaml --profile dev ps --format '{{.Service}}' | sortdocker compose -f compose.tools.yaml --profile dev down >/dev/null 2>&1# بدون پروفایل:Container lxenv3-web-1 Startedweb# با --profile dev:Container lxenv3-debug-1 StartedContainer lxenv3-web-1 Starteddebugwebبدون پروفایل فقط web اجرا شد؛ با --profile dev سرویس debug هم آمد. بهجای پروفایل میتوانی متغیر COMPOSE_PROFILES=dev را هم بگذاری (مثلاً در .env).
مثال ۶: فایل override برای dev و prod
Section titled “مثال ۶: فایل override برای dev و prod”Compose بهطور خودکار compose.yaml را با compose.override.yaml (اگر باشد) ادغام میکند. فایل پایهی مشترک:
name: lxovr
services: web: image: nginx:alpine restart: unless-stoppedتنظیمات خاص dev (بهصورت خودکار خوانده میشود):
services: web: ports: - "8386:80" environment: DEBUG: "1"تنظیمات prod (فقط وقتی صریحاً با -f بدهی):
services: web: ports: - "8387:80" environment: DEBUG: "0"cd ovrecho "# 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 خودکار خوانده نمیشود، و فایلهای بعدی روی قبلیها ادغام میشوند.
پشت پرده
Section titled “پشت پرده”ادغام فایلها چطور کار میکند؟ Compose فایلها را به ترتیب میخواند و روی هم میریزد:
- مقدار تکی (مثل
image،command): فایل بعدی جایگزین میشود. - map (مثل
environmentبهشکل کلید/مقدار): کلیدها ادغام میشوند؛ کلید مشترک مقدار فایل بعدی را میگیرد. - list (مثل
ports،volumes): ردیفها به هم اضافه میشوند (نه جایگزین). برای همین اگر override پورت بدهد و پایه هم پورت داشته باشد، هر دو باز میشوند. اگر میخواهی جایگزین کنی، پایه را بدونportsبنویس (مثل مثال بالا).
نکتهی بسیار مهم: .env فقط برای جایگزینی داخل فایل Compose است. اگر .env شامل DB_PASSWORD باشد ولی در سرویس نه environment: و نه env_file: گذاشته باشی، آن متغیر داخل کانتینر نیست. (درسآموز: docker compose run --rm svc printenv را بزن و ببین.)
جدولهای مرجع
Section titled “جدولهای مرجع”| روش | کجا خوانده میشود | چه میکند |
|---|---|---|
.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 |
$$ |
علامت دلار واقعی |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) متغیر تعریفنشده
Section titled “۱) متغیر تعریفنشده”mkdir -p m1 && printf 'services:\n a:\n image: alpine\n environment:\n X: ${NOPE}\n' > m1/compose.yamlcd 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 داخل کانتینر”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.yamlcd m2 && docker compose run --rm a 2>&1 | grep -vE 'Container|Network'SECRET=[]SECRET در .env هست ولی در سرویس نیامده، پس داخل کانتینر خالی است. راهحل: در environment: یا env_file: تعریفش کن.
۳) فراموش کردن $$ برای متغیرهای داخل کانتینر
Section titled “۳) فراموش کردن $$ برای متغیرهای داخل کانتینر”در مثال بالا $$SECRET نوشتیم تا Compose آن را جایگزین نکند و به شل داخل کانتینر برسد. با یک $ تنها، Compose سعی میکند خودش جایگزین کند. راهحل: برای $ واقعی $$ بنویس.
۴) رمز در Git
Section titled “۴) رمز در Git”اگر .env را commit کردی، حتی بعد از حذف در تاریخچه میماند. راهحل: از اول .gitignore، و .env.example برای راهنما؛ اگر رمز لو رفت، رمز را عوض کن (حذف از Git کافی نیست).
۵) ports در override که به پایه اضافه میشود
Section titled “۵) ports در override که به پایه اضافه میشود”اگر compose.yaml پورت ۸۰:۸۰ و override پورت ۸۰۸۰:۸۰ بدهد، هر دو باز میشوند (list ها اضافه میشوند). راهحل: پورت را فقط در یک فایل بگذار، یا پایه را بدون ports بنویس.
با .env و ${PORT:-9000} پورتی بساز که بدون .env برابر ۹۰۰۰ باشد. با docker compose config هر دو حالت را نشان بده.
دیدن جواب
mkdir -p e1 && printf 'services:\n web:\n image: nginx:alpine\n ports:\n - "${PORT:-9000}:80"\n' > e1/compose.yamlcd e1echo "بدون .env: $(docker compose config | grep published)"printf 'PORT=9100\n' > .envecho "با .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 تست کن.
دیدن جواب
mkdir -p e2 && cd e2printf 'name: lxe2\nservices:\n web:\n image: nginx:alpine\n' > compose.yamlprintf 'services:\n web:\n ports: ["8388:80"]\n environment:\n LOG_LEVEL: debug\n' > compose.override.yamlprintf 'services:\n web:\n ports: ["8389:80"]\n environment:\n LOG_LEVEL: warn\n' > compose.prod.yamlecho "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 2echo "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>&1dev : 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 میآید.
دیدن جواب
mkdir -p e3 && cd e3cat > compose.yaml <<'LXEOF'name: lxe3services: app: image: alpine environment: TOKEN: ${TOKEN:?TOKEN لازم است} command: printenv TOKEN tools: image: alpine command: sleep 5 profiles: [dev]LXEOFecho "۱) $(docker compose config 2>&1 | head -1)"printf 'TOKEN=abc123\n' > .envecho "۲) $(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آزمونک
Section titled “آزمونک”فایل .env چه میکند؟
برای رسیدن به کانتینر باید environment: یا env_file: بگذاری.
اگر در env_file و environment هر دو APP_COLOR بیاید، کدام برنده است؟
environment قویتر است.
کدام سینتکس اگر متغیر نبود خطا میدهد؟
برای رازها و مقدارهای ضروری.
سرویس با profiles: [dev] چه زمانی بالا میآید؟
یا COMPOSE_PROFILES=dev.
با docker compose -f a.yaml -f b.yaml فایل compose.override.yaml…
override خودکار فقط وقتی -f نداری.
در ادغام فایلها ports (یک list) چه میشود؟
پورت را فقط در یک فایل بگذار.
جمعبندی
Section titled “جمعبندی”- ساختار در
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 | فهرست سرویسهای فعال |