Docker Compose با چند فایل و ادغام تنظیمات
یاد بگیرید compose.override.yaml چگونه بهتنهایی بارگیری میشود، ترتیب فایلها چگونه ادغام را تغییر میدهد، چرا ports پورت را باز نگه میدارد و include چه نقشی دارد.
Compose با بیش از یک فایل چه کاری انجام میدهد
Docker Compose میتواند یک پروژه را از چند فایل بسازد. فایلها را به ترتیبی که دریافت میکند میخواند و آنها را در یک مدل واحد ادغام میکند؛ بنابراین در هر مقدار متعارض، فایل بعدی اولویت دارد. این کار از خط فرمان با دو سازوکار انجام میشود: یک فایل override که Compose آن را بهصورت خودکار بارگیری میکند، و پرچم -f که خودتان مشخص میکنید. سازوکار سوم درون خود فایل قرار دارد: عنصر include. این سازوکار با دو مورد دیگر متفاوت است.
ادغام، بازنویسی ساده نیست. نگاشتها کلیدبهکلید ادغام میشوند، دنبالهها به انتهای یکدیگر افزوده میشوند و مجموعه کوچکی از فیلدها بهطور کامل جایگزین میشوند. تفاوت میان این رفتارها منشأ بیشتر شگفتیها است و فهرست ports بیش از همه کاربران را دچار مشکل میکند.
تمام مطالب زیر بر مبنای Compose v2 است؛ یعنی افزونه docker compose، نه اسکریپت قدیمی docker-compose. برای بررسی، docker compose version را اجرا کنید. اگر هنوز فایل Compose ننوشتهاید، از راهنمای مبانی Docker Compose شروع کنید و سپس به این بخش بازگردید.
فایل override که Compose بدون اعلام قبلی بارگذاری میکند
docker compose up را بدون پرچم -f اجرا کنید تا Compose در پوشه کاری و سپس در پوشههای والد آن، بهدنبال compose.yaml یا docker-compose.yaml بگردد. اگر فایل override کنار فایل پایه قرار داشته باشد، Compose آن را نیز بهصورت خودکار بارگذاری میکند.
ls compose.yaml compose.override.yaml
docker compose up -dوقتی هر دو فایل وجود داشته باشند، نتیجه با وارد کردن دستی هر دو فایل یکسان است.
docker compose -f compose.yaml -f compose.override.yaml up -dنامهایی که Compose میشناسد عبارتاند از compose.override.yaml، compose.override.yml و نامهای قدیمیتر docker-compose.override.yml و docker-compose.override.yaml. هر نام دیگری، مانند compose.dev.yaml، فقط زمانی بارگذاری میشود که آن را با -f مشخص کنید.
بهمحض اینکه یک -f را تعیین کنید، بارگذاری خودکار متوقف میشود. docker compose -f compose.yaml up دقیقاً همان یک فایل را میخواند و فایل override را نادیده میگیرد. الگوی dev و prod که در ادامه این راهنما میآید، بر همین ویژگی بنا شده است.
این رفتار در سرور میتواند پیامدهای مثبت و منفی داشته باشد. اگر فایل override در پوشه استقرار باقی بماند، هر دستور ساده docker compose که از آن پوشه اجرا شود، آن را بارگذاری میکند؛ از جمله دستوری که cron job شما اجرا میکند. در این حالت، ممکن است یک stack در محیط production در نهایت یک پوشه source را بهصورت bind mount متصل کند، درحالیکه قرار نبوده استقرار یابد. پس از هر استقرار، docker compose config را اجرا کنید و خروجی ایجادشده را بخوانید.
ترتیب با -f و محل resolve شدن مسیرهای نسبی
Compose پیکربندی را بر اساس ترتیبی که فایلها را ارائه میکنید میسازد. فایلهای بعدی، تنظیمات فایلهای قبلی را بازنویسی میکنند و تنظیمات جدیدی به آنها میافزایند. ترتیب از چپ به راست است و آخرین مقدار برنده میشود.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dهر فرمانی در آن پروژه باید از همان فهرست فایلها استفاده کند. اجرای up با دو فایل و logs با یک فایل، شما را به یک مدل ادغامشده متفاوت متصل میکند. این کار راه سریعی برای رسیدن به وضعیتی است که Compose میگوید یک سرویس وجود ندارد. بهجای آن، فهرست را یکبار با متغیر محیطی COMPOSE_FILE تنظیم کنید.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dجداکننده در Linux برابر با : است و COMPOSE_PATH_SEPARATOR آن را تغییر میدهد. COMPOSE_FILE میتواند در فایل .env پروژه نیز قرار بگیرد. در این حالت، این تنظیم بخشی از checkout است، نه بخشی از سابقه shell شما. هر مقداری که بهطور صریح در خط فرمان تنظیم شود، بر متغیر محیطی اولویت دارد.
اکنون به قاعدهای میرسیم که mountهای bind را دچار مشکل میکند. هنگام استفاده از چند فایل با -f، همه مسیرهای نسبی در تمام فایلها نسبت به پوشه فایل اول resolve میشوند، نه نسبت به فایلی که آن مسیرها را دربر دارد. اگر ./data:/var/lib/postgresql/data را داخل deploy/prod/compose.prod.yaml بنویسید، Compose همچنان به دنبال ./data در کنار فایل پایه میگردد. سپس Docker در مسیر اشتباه یک پوشه خالی ایجاد میکند و container بدون هیچ دادهای در آن راهاندازی میشود. این وضعیت شبیه از دست رفتن داده است، اما از دست رفتن داده رخ نداده است. برای تعیین دستی مسیر پایه، --project-directory را ارسال کنید. همچنین میتوانید از include استفاده کنید که هر فایل را نسبت به پوشه خودش resolve میکند.
نام پروژه از همان پوشه پایه گرفته میشود. بنابراین، تغییر فایل اول میتواند نام پروژه را تغییر دهد. تغییر نام پروژه باعث ایجاد نامهای جدید برای containerها و volumeها میشود. volume قدیمی همچنان روی دیسک و با نام قبلی باقی میماند. برای ثابت نگهداشتن نام، یک name: سطحبالا را در فایل پایه تعیین کنید.
name: myappکدام فیلدها ادغام میشوند و کدام فیلدها جایگزین میشوند
Compose بر اساس نوع مقدار ادغام میکند، نه بر اساس نام فیلد.
- فیلدهای تکمقداری جایگزین میشوند.
image،command،entrypointوmem_limitمقدار بعدی را بهطور کامل میپذیرند. نمیتوانید یک آرگومان را بهcommandاضافه کنید، زیرا بازنویسی، کل خط را جایگزین میکند. - نگاشتها بر اساس کلید ادغام میشوند.
environment،labels،volumesوdevicesهمه کلیدهای هر دو فایل را نگه میدارند و در صورت وجود یک کلید در هر دو فایل، فایل بعدی برنده است. برایenvironmentوlabels، کلید نام متغیر یا برچسب است. برایvolumesوdevices، کلید مسیر کانتینر است. - دنبالهها به هم الحاق میشوند.
dns،dns_search،expose،tmpfsوexternal_linksبه هم متصل میشوند. اگر پایه شاملexpose: ["3000"]باشد و با جایگزینی شامل["4000", "5000"]ادغام شود، نتیجه["3000", "4000", "5000"]خواهد بود.
چهار دنباله دارای کلید هویتی هستند؛ بنابراین ورودیهایی که بر اساس آن کلید مطابقت دارند، بهجای الحاق، ادغام میشوند. volumes، secrets و configs بر اساس target مطابقت داده میشوند. ports بر اساس ترکیب ip، target، published و protocol مطابقت داده میشود.
قانون ports را دو بار بخوانید، زیرا نکته گمراهکننده همینجاست. دو ورودی پورت فقط زمانی یک ورودی یکسان محسوب میشوند که هر چهار بخش با هم مطابقت داشته باشند. اگر هر یک از آنها را تغییر دهید، Compose آن را یک پورت دوم و مستقل در نظر میگیرد؛ بنابراین هر دو را نگه میدارد.
چرا پورت شما پس از override همچنان منتشر میشود
یک فایل پایه که سرویسی را روی همه رابطها منتشر میکند:
services:
web:
image: nginx:1.27
ports:
- "8080:80"یک override که سرویس را فقط به localhost متصل میکند، زیرا یک reverse proxy در جلوی آن قرار خواهد گرفت:
services:
web:
ports:
- "127.0.0.1:8080:80"پیش از آنکه فرض کنید تنظیمات اعمال شده است، نتیجه را بررسی کنید.
docker compose -f compose.yaml -f compose.prod.yaml configهر دو ورودی در خروجی وجود دارند. بخش ip متفاوت است؛ 0.0.0.0 در برابر 127.0.0.1. بنابراین از دید فرایند ادغام، اینها دو پورت متفاوت هستند و اتصال عمومیای که تلاش کردید حذف کنید، همچنان در مدل وجود دارد. این موضوع در Docker اهمیت بیشتری دارد، زیرا یک پورت منتشرشده پیش از قوانین فایروال شما در iptables نوشته میشود. سازوکار آن در دلیل عبور پورتهای منتشرشده Docker از ufw توضیح داده شده است.
دو راهحل وجود دارد. راهحل صریح استفاده از برچسب !override است که کل ویژگی را جایگزین میکند و قوانین ادغام را نادیده میگیرد:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override به Compose v2.24.4 یا جدیدتر نیاز دارد. راهحل قابلحمل به هیچ برچسبی نیاز ندارد: ports را بهطور کامل از فایل پایه خارج کنید و آن را فقط در فایلهای مخصوص هر محیط تعریف کنید. وقتی چیزی برای ادغام وجود نداشته باشد، چیزی هم نشت نمیکند. در مثال کامل زیر از همین الگو استفاده شده است.
حذف مقدار تعیینشده در فایل پایه
!reset یک ویژگی را حذف میکند و آن را به مقدار پیشفرض یا null برمیگرداند. این دستور یک مقدار دریافت میکند و آن را نادیده میگیرد؛ بنابراین مقداری معتبر و خالی بنویسید.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset به Compose نسخه 2.24 یا جدیدتر نیاز دارد. زمانی از آن استفاده کنید که فایل پایه متعلق به شما نیست و برای نمونه، یک قطعه پیکربندی فروشنده را وارد میکنید.
اجزا برای پشتههایی که از چند بخش ساخته میشوند
include یک برنامه Compose دیگر را به مدل شما اضافه میکند. این گزینه یک عنصر سطحبالا است، نه یک flag.
include:
- path: ../commons/compose.yamlهر مسیر در include بهعنوان یک مدل مستقل از برنامه Compose بارگذاری میشود و دایرکتوری پروژه مستقل خود را دارد. بنابراین مسیرهای نسبی داخل آن فایل نسبت به دایرکتوری همان فایل resolve میشوند. این تفاوت اصلی با -f است و به همین دلیل، وقتی fragment در پوشه یا repository دیگری قرار دارد، include ابزار مناسبتری است.
فرم کامل، زیربررسیهایی دارد.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath یک فهرست میپذیرد و این فایلها طبق قوانین معمول با یکدیگر merge میشوند؛ سپس نتیجه به مدل شما اضافه میشود. project_directory مسیر پایهای را برای resolve کردن مسیرهای نسبی در فایل included تعیین میکند. env_file متغیرهای اختصاصی فایل included را برای interpolation مشخص میکند. این کار مانع میشود یک fragment مشترک، متغیر .env پروژه شما را بدون اطلاع بخواند. include به Compose v2.20.0 یا جدیدتر نیاز دارد.
اگر نام resource در فایل شما و فایل included تکراری باشد، این مورد بهجای merge شدن بیصدا، بهعنوان خطا گزارش میشود. این رفتار عمدی است. برای تغییر چیزی که فایل included تعریف کرده است، تغییر را در compose.override.yaml قرار دهید. override روی مدل assembled اعمال میشود؛ بنابراین میتواند resourceهای included را بدون ایجاد تعارض تغییر دهد.
خلاصه: include برنامههای جداگانه را با هم compose میکند، اما -f پیکربندی را روی یک برنامه اعمال میکند.
تفکیک توسعه و تولید روی یک VPS
الگوی کامل در این 3 فایل آمده است. فایل پایه مشخص میکند که در همهجا چه چیزی برقرار است و هیچ پورتی را منتشر نمیکند.
name: myapp
services:
app:
image: ghcr.io/example/app:1.4.2
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:شرط depends_on باعث میشود برنامه منتظر پایگاهدادهای بماند که پاسخ میدهد، نه کانتینری که فقط وجود دارد؛ این موضوع در بررسی سلامت و شرطهای depends_on توضیح داده شده است. مقدار POSTGRES_PASSWORD از فایل .env پروژه درونیابی میشود؛ این فایل هرگز نباید در git قرار بگیرد. برای گزینههای امنتر، فایلهای محیطی و secrets در Compose را ببینید.
سپس compose.override.yaml قرار دارد که Compose آن را بهصورت خودکار بارگذاری میکند. این فایل مخصوص توسعهدهنده است.
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
volumes:
- ./src:/app/src
db:
ports:
- "127.0.0.1:5432:5432"در لپتاپ، اجرای ساده docker compose up این دو فایل را با هم ادغام میکند. command مقدار پیشفرض image را جایگزین میکند، چون تکمقداری است. LOG_LEVEL جایگزین info میشود، چون environment بر اساس کلید ادغام میشود. bind mount و 2 پورت منتشرشده صرفاً اضافه میشوند. پورت پایگاهداده نیز به localhost متصل میشود تا لپتاپی در یک شبکه اشتراکی، PostgreSQL را برای سایرین در دسترس قرار ندهد.
در پایان، compose.prod.yaml قرار دارد. Compose به دنبال فایلی با این نام نمیگردد؛ بنابراین این فایل بهصورت تصادفی بارگذاری نمیشود.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512Mدر VPS هر دو فایل را نام میبرید و همین نامگذاری دقیقاً باعث میشود override کنار گذاشته شود.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml psps باید هر دو سرویس را در وضعیت running فهرست کند و db مقدار (healthy) را نشان دهد. چون -f را ارسال کردید، compose.override.yaml خوانده نشد. بنابراین command توسعه، bind mount کد منبع و پورت عمومی 3000 نمیتوانند به production راه پیدا کنند، حتی اگر فایل در همان پوشه قرار داشته باشد. پورت 8000 فقط روی localhost قرار دارد و برای proxy آماده است. هنگام افزودن سرویس دوم، اجرای چند برنامه پشت Traefik را ببینید.
COMPOSE_FILE=compose.yaml:compose.prod.yaml را در .env سرور تنظیم کنید تا بقیه commandهای شما دوباره به docker compose logs -f app ساده تبدیل شوند.
پیش از استقرار، مدل ادغامشده را بخوانید
docker compose config مدل کاملاً ادغامشده و کاملاً درونیابیشده را چاپ میکند. این خروجی پیشنمایش نیست. این همان ورودی دقیقی است که Compose بر اساس آن عمل میکند؛ بنابراین اگر خروجی با انتظار شما مطابقت نداشته باشد، خروجی درست است.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services--no-interpolate مقدار ${VAR} را گسترشنیافته باقی میگذارد. پیش از جایگذاری خروجی در هر مکان از آن استفاده کنید، زیرا config ساده همه secretهای resolveشده را بهصورت متن آشکار چاپ میکند. --services فقط نام سرویسها را فهرست میکند و راهی سریع برای تأیید این است که یک include موارد مورد انتظار شما را وارد کرده است.
حالتهای شکست و آنچه مشاهده خواهید کرد
no configuration file provided: not found. Compose چیزی برای خواندن پیدا نکرد. خارج از دایرکتوری پروژه هستید، یا COMPOSE_FILE مسیری را مشخص میکند که وجود ندارد. Compose برای یافتن فایل پایه پیشفرض، دایرکتوریهای والد را جستوجو میکند؛ اما برای فایلی که خودتان نام بردهاید، هیچ دایرکتوری دیگری را جستوجو نمیکند.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. جایگزینی متغیرها بر اساس فایل پروژه .env و محیط shell انجام میشود، و دایرکتوری پروژه در اینجا همان دایرکتوری فایل -f اول است. اگر استقرار را از دایرکتوری دیگری نسبت به دایرکتوری حاوی .env انجام دهید، این هشدار را دریافت میکنید و سپس پایگاه دادهای ایجاد میشود که همه اتصالها را رد میکند.
ویرایش override شما در docker compose config نمایش داده نمیشود. یا -f را ارسال کردهاید که بارگذاری خودکار override را غیرفعال میکند، یا Compose فایل compose.yaml را در یک دایرکتوری والد پیدا کرده است و فایل override شما در کنار آن قرار ندارد. اجرای docker compose config بدون آرگومانهای دیگر نشان میدهد Compose واقعاً در حال ساختن کدام مدل است.
یک bind mount خالی است و Docker دایرکتوریای ایجاد کرده که درخواست نکردهاید. مسیر نسبی بر اساس دایرکتوری فایل اول تفسیر شده است. مسیر را اصلاح کنید، --project-directory را ارسال کنید، یا fragment را به پشت include منتقل کنید.
Containerها با نامهای جدید برمیگردند و یک volume خالی به نظر میرسد. نام پروژه تغییر کرده است، زیرا نام پروژه از دایرکتوری فایل اول پیروی میکند. یک name: سطحبالا به فایل پایه اضافه کنید تا نامگذاری دیگر تغییر نکند. volume قدیمی همچنان با پیشوند قبلی وجود دارد و docker volume ls آن را نمایش میدهد.
Portی که در override حذف کردهاید همچنان باز است. ادغام ports بهجای جایگزینی، آن را اضافه کرده است. با docker compose config تأیید کنید، سپس یا از !override استفاده کنید یا ports را از فایل پایه خارج کنید.
FAQ
آیا Compose فایل compose.override.yaml را بهصورت خودکار بارگذاری میکند؟
بله، وقتی docker compose را بدون پرچم -f اجرا میکنید. Compose در دایرکتوری کاری و والدهای آن، compose.yaml یا docker-compose.yaml را جستوجو میکند و اگر فایل override در کنار آن قرار داشته باشد، آن فایل را در مرحله دوم بارگذاری میکند. نامهای شناختهشده عبارتاند از compose.override.yaml، compose.override.yml، docker-compose.override.yml و docker-compose.override.yaml. ارسال هر -f این رفتار را غیرفعال میکند؛ بنابراین docker compose -f compose.yaml up فقط یک فایل را میخواند.
فایلهای متعدد -f با چه ترتیبی ادغام میشوند؟
از چپ به راست. Compose پیکربندی را بر اساس ترتیبی که فایلها را ارائه میکنید میسازد. هر فایل، موارد فایلهای قبلی را بازنویسی میکند و مواردی به آنها میافزاید؛ بنابراین در هر تعارض، آخرین فایل موجود در خط فرمان اولویت دارد. همین فهرست باید برای همه فرمانهای آن پروژه استفاده شود؛ COMPOSE_FILE=compose.yaml:compose.prod.yaml برای همین منظور است.
چرا پس از بازنویسی، پورت من همچنان منتشر میشود؟
زیرا ورودیهای ports بر اساس مجموعه کامل ip، target، published و protocol شناسایی میشوند. بازنویسی 127.0.0.1:8080:80 در برابر مقدار پایه 8080:80 در بخش ip تفاوت دارد؛ بنابراین Compose آن را یک پورت دوم در نظر میگیرد و هر دو را نگه میدارد. docker compose config را اجرا کنید تا دو ورودی را ببینید. در Compose v2.24.4 یا جدیدتر از ports: !override استفاده کنید، یا ports را از فایل پایه حذف کنید تا چیزی برای ادغام با آن وجود نداشته باشد.
تفاوت include و -f چیست؟
-f چند فایل را روی یک برنامه لایهبندی میکند و مسیر نسبی در هر فایل، نسبت به دایرکتوری فایل اول resolve میشود. include یک برنامه Compose جداگانه را وارد میکند و هر مسیر واردشده، دایرکتوری پروژه خودش را حفظ میکند؛ بنابراین مسیرهای نسبی آن نسبت به همان برنامه resolve میشوند. برای لایههای محیطی در stack خودتان از -f استفاده کنید و برای fragmentای که در محل دیگری نگهداری میشود، include را بهکار ببرید. include به Compose v2.20.0 یا جدیدتر نیاز دارد.
چگونه مقداری را که فایل پایه تنظیم کرده است حذف کنم؟
در Compose v2.24 یا جدیدتر، از تگ !reset استفاده کنید. در فایل override، ports: !reset [] یا MY_VAR: !reset null را بنویسید تا attribute به مقدار پیشفرض یا null بازگردد. مقداری که به این تگ میدهید الزامی است، اما نادیده گرفته میشود. اگر میخواهید یک attribute را بهجای پاککردن، جایگزین کنید، !override این کار را انجام میدهد و به v2.24.4 یا جدیدتر نیاز دارد.