تفاوت .env و env_file و environment در Docker Compose
تفاوت دقیق بین .env و env_file و environment را در Docker Compose بیاموزید. این راهنما اولویتبندی متغیرها و محل صحیح ذخیره رمزهای عبور برای امنیت کانتینرها را بررسی میکند.
سه مفهومی که مردم به آن فایل env میگویند
Docker Compose سه مکانیزم مجزا با نامهای گیجکننده مشابه دارد. فایل .env جایخالیهای ${VARIABLE} را در داخل خود compose.yaml پر میکند؛ این اتفاق پس از خواندن فایل YAML توسط Compose و پیش از اعتبارسنجی یا اجرای هر چیزی رخ میدهد. ویژگی env_file: فایلی شامل جفتهای کلید/مقدار را در محیط (environment) کانتینر بارگذاری میکند. ویژگی environment: متغیرها را مستقیماً روی کانتینر تنظیم میکند که در فایل compose نوشته شدهاند. این موارد قابل جایگزینی با یکدیگر نیستند و زمانی که دو مورد از آنها یک کلید مشابه را تنظیم کنند، برنده بر اساس یک ترتیب اولویت مستندشده تعیین میشود.
این راهنما نحوه عملکرد هر کدام را نشان میدهد، اولویت آنها را با دستوری که میتوانید اجرا کنید اثبات میکند و سپس به بخش مهمتر میپردازد: متغیرهای محیطی توسط هر کسی که بتواند docker inspect را اجرا کند قابل خواندن هستند، بنابراین رمزهای عبور نباید در آنها قرار بگیرند. اگر در زمینه فایلهای compose تازهکار هستید، با مبانی Docker Compose روی یک VPS شروع کنید و سپس برای پیکربندی به اینجا بازگردید.
فایل .env برای فایل compose است، نه برای container
یک دایرکتوری ایجاد کنید و دو فایل در آن قرار دهید.
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAGاکنون از Compose بپرسید که در واقع چه چیزی را تجزیه (parse) کرده است.
docker compose configخروجی image: alpine:3.20 را نشان میدهد. جایگیر (placeholder) حذف شده است، زیرا جایگذاری (interpolation) هنگام بارگذاری فایل توسط Compose انجام شده است. Compose به دنبال .env در دایرکتوری پروژه میگردد: دایرکتوری که با --project-directory تعیین شده است (اگر آن را تنظیم کرده باشید)، در غیر این صورت دایرکتوری اولین فایلی که با -f ارسال شده، و در غیر این صورت دایرکتوری فعلی شما. این ابزار جایگیرهای ${NAME} را که در مقادیر YAML پیدا میکند، جایگذاری مینماید. کلیدها هرگز جایگذاری نمیشوند و یک $$ مضاعف، به جای جایگذاری، یک علامت دلار واقعی تولید میکند.
سپس سرویس را اجرا کنید.
docker compose run --rm demoprintenv ALPINE_TAG با وضعیت 1 خارج میشود و چیزی چاپ نمیکند. متغیر در داخل container وجود ندارد. این رایجترین سوءتفاهم است: .env فایل compose را پیکربندی کرده است، نه فرآیند (process) داخل آن را. یک فایل .env که حاوی POSTGRES_PASSWORD=hunter2 باشد، هیچ کاری برای دیتابیس شما انجام نمیدهد مگر اینکه بخشی از فایل compose به آن ارجاع دهد.
${NAME:-default} یک مقدار پیشفرض (fallback) ارائه میدهد که در صورت تنظیم نبودن یا خالی بودن متغیر استفاده میشود. ${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 است، در هر خط یک مورد، و # نشاندهنده شروع یک توضیح (comment) است. این فرمت shell نیست، اگرچه Compose آن را با چند قاعده مشابه shell تجزیه میکند. کوتیشنهای اطراف حذف میشوند، بنابراین KEY="value" و KEY=value نتیجه یکسانی میدهند. مقادیر داخل دابلکوتیشن و مقادیر بدون کوتیشن، جایگذاری ${...} را انجام میدهند، در حالی که مقادیر داخل سینگلکوتیشن به صورت تحتاللفظی استفاده میشوند. پیشوند export پذیرفته میشود اما الزامی نیست. فاصلههای اطراف علامت = حذف میشوند. نکتهای که باید به آن توجه کرد، توضیحات درونخطی است: در یک مقدار بدون کوتیشن، یک فاصله که با # دنبال شود، شروع یک توضیح است؛ بنابراین هر مقداری که شامل # است را داخل کوتیشن قرار دهید.
نبود مسیر env_file یک خطا محسوب میشود و Compose متوقف میگردد. اگر ممکن است فایل به صورت قانونی وجود نداشته باشد، آن را اختیاری علامتگذاری کنید:
env_file:
- path: ./app.env
required: falseتنظیم متغیرهای محیطی به صورت inline
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentدو نحو (syntax) برای این کار پذیرفته شده است: فرم نگاشت (mapping) که در بالا آمد و فرم لیست که از - GREETING=from_environment استفاده میکند. عملکرد هر دو یکسان است. هر دو از یک ترفند اضافی پشتیبانی میکنند: کلیدی که مقداری ندارد، متغیر را از shellای که دستور docker compose را در آن اجرا کردهاید، به داخل منتقل میکند. در فرم لیست، این کار با یک - GREETING ساده و در فرم نگاشت با یک GREETING: که بعد از دونقطه (colon) چیزی ندارد، انجام میشود.
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoاین دستور مقدار from_my_shell را چاپ میکند. اگر آن را بدون تنظیم GREETING در shell اجرا کنید، Compose هیچ مقداری تنظیم نمیکند و هشداری هم نمیدهد. آگاهی از این شکستهای خاموش (silent pass-through failures) ضروری است، زیرا سرویسی که با یک متغیر رمز عبور خالی شروع میشود، اغلب با موفقیت بالا میآید اما کاملاً در دسترس و ناامن باقی میماند.
کدام اولویت دارد
داکیومنتهای Docker ترتیب اولویت را از بالاترین به پایینترین به این صورت مشخص کردهاند: docker compose run -e در خط فرمان، سپس environment یا env_file که مقدار آنها از shell شما یا یک فایل env جایگذاری (interpolate) میشود، سپس 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 متغیرهای جایگذاری که 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" به صورت متن ساده (plain text) است. سه مسیر دیگر نیز همین مقدار را افشا میکنند. دستور docker compose config آن را در ترمینال چاپ میکند؛ این همان روشی است که باعث میشود رمز عبور در انجمنهای پشتیبانی کپی و پیست شود. فرآیندهای فرزند بهطور پیشفرض متغیر را به ارث میبرند، مگر اینکه والد عمداً آنها را با محیطی جایگزین اجرا کند. همچنین، فرآیندی که با همان کاربر PID 1 اجرا شود یا دارای قابلیت CAP_SYS_PTRACE باشد، میتواند آن را از /proc/1/environ بخواند. در کانتینری که همه چیز با کاربر root اجرا میشود، این شامل تمام فرآیندهاست. برنامهای که با یک کاربر بدون امتیاز (unprivileged) اجرا میشود در حالی که PID 1 با root اجرا میشود، هنگام دسترسی به آن فایل با خطای مجوز مواجه میشود، اگرچه همچنان متغیر را در محیط خود نگه میدارد. علاوه بر این، مدیریتکنندههای خطای برنامه (crash handlers) معمولاً کل محیط را در یک لاگ یا گزارش خطا تخلیه (dump) میکنند.
عضویت در گروه docker عملاً به معنای دسترسی root روی میزبان است، بنابراین این یک مرز امنیتی نیست که بتوانید به آن تکیه کنید. راهنمای حسابهای کاربری با حداقل امتیاز در VPS توضیح میدهد که چرا محدود کردن این گروه در هر سیستم اشتراکی ضروری است.
نگهداری مقادیر secrets در فایل با Compose
ابزار Compose از secrets مبتنی بر فایل پشتیبانی میکند. مقدار secret بهجای تزریق در متغیرهای محیطی (environment)، بهعنوان یک فایل در داخل container مانت (mount) میشود.
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 در داخل container مانت میشود. نام پس از اسلش، همان نام secret تعریفشده در بلوک سطح بالای secrets: است.
پسوند _FILE یک قرارداد است که توسط تصاویر رسمی Docker از جمله postgres، mysql و mariadb استفاده میشود. اسکریپتهای entrypoint در این تصاویر، وجود VARNAME_FILE را بررسی کرده، فایل را میخوانند و از محتوای آن استفاده میکنند. این قابلیت، ویژگی ذاتی Docker نیست و تنها در صورتی کار میکند که تصویر (image) آن را پیادهسازی کرده باشد. پیش از آنکه فرض کنید SOMETHING_FILE توسط برنامه رعایت میشود، مستندات تصویر را بررسی کنید. برنامههایی که از این قابلیت پشتیبانی نمیکنند، اغلب میتوانند خودشان فایل را در زمان راهاندازی بخوانند، یا میتوانید مسیر فایل را به آنها بدهید تا entrypoint اختصاصی شما آن را پردازش کند.
بررسی از داخل container در حال اجرا:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORDدستور اول رمز عبور را چاپ میکند. دستور دوم چیزی چاپ نمیکند، زیرا مقدار هرگز وارد محیط (environment) نشده است. هدف اصلی همین است: اجرای docker inspect روی این container فقط مسیر فایل را نشان میدهد که حاوی اطلاعات حساس نیست.
از فایل منبع روی میزبان (host) محافظت کنید، زیرا امنیت secret به اندازه امنیت فایلی است که آن را در خود نگه میدارد:
chmod 600 db_password.txtرویکرد میانه و عملگرایانه در یک VPS
بسیاری از ایمیجهای self-hosted از متغیرهای _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 را اجرا کند، همچنان قادر است مقدار را از داخل container بخواند. فایلهای *.env و .env را به .gitignore اضافه کنید و به جای آن، یک app.env.example شامل نام کلیدها با مقادیر خالی کامیت کنید. رمز عبوری که کامیت شده باشد، رمز عبوری است که باید تغییر (rotate) داده شود.
تغییر دادن (Rotating) یک مقدار به معنای راهاندازی مجدد سرویس است. متغیرهای محیطی تنها یکبار در زمان شروع پردازش container خوانده میشوند، بنابراین ویرایش فایل تا زمانی که docker compose up -d --force-recreate db را اجرا نکنید، هیچ تغییری ایجاد نمیکند. این همان الگویی است که در راهنمای n8n behind HTTPS on a VPS استفاده شده است، جایی که کلید رمزنگاری خارج از فایل compose قرار میگیرد.
تفکیک پیکربندی بر اساس محیط
برنامه Compose بهصورت پیشفرض فایل .env را از دایرکتوری پروژه میخواند. برای ارجاع به مسیری دیگر، از --env-file استفاده کنید.
docker compose --env-file .env.staging configفایلهای متعدد به ترتیب خوانده میشوند و فایلهای بعدی، تنظیمات فایلهای قبلی را بازنویسی میکنند. مقادیر پیشفرض غیرحساس را در فایلی که در مخزن کد (commit) قرار میگیرد نگهداری کنید و موارد محرمانه را در فایلی بگذارید که هرگز از سرور خارج نمیشود. همین قاعده در مورد 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 نهایی با تمام جایگذاریهای اعمالشده چاپ شود. برای کانتینری که در حال اجرا است، دستور docker inspect <container> --format '{{json .Config.Env}}' دقیقاً نشان میدهد که فرآیند آن چه مقادیری را دریافت کرده است.
آیا secrets در Compose رمزنگاری شدهاند؟
خیر. یک secret مبتنی بر فایل، به عنوان یک فایل متنی ساده در مسیر /run/secrets/<name> داخل کانتینر mount میشود و فایل منبع نیز به صورت رمزنگارینشده روی دیسک میزبان قرار دارد. مزیت این روش در محدودسازی دامنه دسترسی است، نه رمزنگاری: مقدار مورد نظر از محیط کانتینر، خروجی docker inspect و فایلهای crash dump که محیط را چاپ میکنند، دور میماند.
آیا میتوانم از کوتیشن و فاصله در فایل env استفاده کنم؟
بله. Compose کوتیشنهای اطراف را حذف میکند، بنابراین KEY="value with spaces" و KEY=value with spaces هر دو نتیجه value with spaces را میدهند. زمانی که مقدار شامل $ است که نباید جایگذاری (interpolate) شود، از کوتیشن تکی استفاده کنید و هر مقداری که شامل # است را داخل کوتیشن قرار دهید، زیرا در حالت بدون کوتیشن، آن کاراکتر شروع یک کامنت محسوب میشود. فاصلههای اطراف = حذف میشوند، بنابراین KEY = value همچنان KEY را تنظیم میکند.