تفاوت .env و env_file و environment در Docker Compose
تفاوت دقیق بین .env و env_file و environment را در Docker Compose بیاموزید. این راهنما اولویتبندی متغیرها و محل صحیح ذخیره رمزهای عبور برای امنیت کانتینرها را بررسی میکند.
سه مفهومی که مردم با نام فایل env میشناسند
Docker Compose سه مکانیزم مجزا با نامهای مشابه و گیجکننده دارد. فایل .env جایگذاریهای ${VARIABLE} را درون خود compose.yaml پر میکند، حتی پیش از آنکه Compose فایل را تجزیه (parse) کند. ویژگی 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) در زمان parse انجام شده است. Compose به دنبال .env در دایرکتوری پروژه میگردد، که همان دایرکتوری حاوی فایل compose است، و هر ${NAME} که پیدا کند را جایگزین میکند.
سپس سرویس را اجرا کنید.
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 است، در هر خط یک مورد، و # نشاندهنده شروع یک کامنت است. این فرمت shell نیست. در اکثر موارد، کوتیشنها به عنوان بخشی از مقدار حفظ میشوند و نیازی به پیشوند export نیست. در اطراف علامت = فاصله نگذارید، زیرا KEY = value متغیری ایجاد میکند که نام آن دقیقاً KEY است و یک فاصله در ابتدای مقدار آن قرار میگیرد.
نبود مسیر env_file یک خطا محسوب میشود و Compose متوقف میگردد. اگر ممکن است فایل به صورت قانونی وجود نداشته باشد، آن را به عنوان اختیاری علامتگذاری کنید:
env_file:
- path: ./app.env
required: falseتنظیم متغیرهای محیطی به صورت inline
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentدو نحو برای این کار پذیرفته میشود: فرم نگاشت (mapping) در بالا و فرم لیست که از - 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 جایگذاری (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 آن را در ترمینال چاپ میکند و همین باعث میشود که در انجمنهای پشتیبانی کپی و پیست شود. هر پردازشی درون کانتینر میتواند /proc/1/environ را بخواند و هر پردازش فرزند نیز این متغیر را به ارث میبرد. همچنین، مدیریتکنندههای خطای برنامه (crash handlers) معمولاً کل محیط را در یک لاگ یا گزارش خطا تخلیه میکنند.
عضویت در گروه 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 جایگذاری میکند. این فایل هرگز متغیرها را داخل کانتینر تنظیم نمیکند. برای انتقال مقدار به داخل کانتینر، به آن ارجاع دهید: 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 میشود و فایل منبع نیز بهصورت رمزنگارینشده روی دیسک میزبان قرار دارد. مزیت این روش در محدودهبندی (scope) است، نه رمزنگاری: مقدار از محیط کانتینر، خروجی docker inspect و crash dumpهایی که محیط را چاپ میکنند، دور میماند.
آیا میتوانم از کوتیشن و فاصله در فایل env استفاده کنم؟
از KEY=value with spaces استفاده کنید و کوتیشنها را حذف کنید. Compose تمام ادامه خط را بهعنوان مقدار در نظر میگیرد، بنابراین کوتیشنها معمولاً بهعنوان کاراکترهای متنی در مقدار نهایی قرار میگیرند. هرگز اطراف = فاصله نگذارید، زیرا در این صورت کلید شامل یک فاصله در انتها خواهد بود و هیچچیز با آن مطابقت نخواهد داشت.