Docker Compose-এ একাধিক ফাইল কীভাবে মার্জ হয়
compose.override.yaml কীভাবে নিজে লোড হয়, ফাইলের ক্রমে কোন মান জেতে, ports কেন পোর্ট খোলা রাখে এবং dev ও prod ভাগে include কীভাবে ব্যবহার করবেন।
একাধিক ফাইলের ক্ষেত্রে Compose কী করে
Docker Compose একাধিক ফাইল থেকে একটি প্রজেক্ট তৈরি করতে পারে। এটি ফাইলগুলো যে ক্রমে পায়, সেই ক্রমে পড়ে এবং সেগুলোকে একটি একক মডেলে মার্জ করে। তাই পরের ফাইল কোনো বিরোধপূর্ণ মানের ক্ষেত্রে অগ্রাধিকার পায়। কমান্ড লাইন থেকে এটি করার দুটি ব্যবস্থা আছে: Compose নিজে যে override ফাইল লোড করে, এবং হাতে দেওয়া -f flag। তৃতীয় ব্যবস্থাটি ফাইলের ভেতরেই থাকে, অর্থাৎ include element। এটি অন্য দুটির থেকে ভিন্নভাবে কাজ করে।
এই মার্জ প্রক্রিয়া সরাসরি overwrite নয়। Mapping-গুলো key অনুযায়ী মার্জ হয়, sequence-গুলোতে উপাদান যোগ হয়, আর অল্প কিছু field সম্পূর্ণভাবে প্রতিস্থাপিত হয়। এই পার্থক্য থেকেই অপ্রত্যাশিত ফলাফল দেখা দেয়। ports list-ই প্রায় সবাইকে সবচেয়ে বেশি সমস্যায় ফেলে।
নিচের সবকিছু Compose v2 ধরে লেখা হয়েছে। এখানে পুরোনো docker-compose script-এর বদলে docker compose plugin ব্যবহার করা হয়েছে। পরীক্ষা করতে docker compose version চালান। এখনো Compose file না লিখে থাকলে Docker Compose-এর মৌলিক বিষয়ের নির্দেশিকা দিয়ে শুরু করে পরে এখানে ফিরে আসুন।
Compose যে override file নিজে থেকেই লোড করে
কোনো -f flag ছাড়াই docker compose up চালালে Compose প্রথমে working directory-তে, তারপর তার parent directory-গুলোতে compose.yaml বা docker-compose.yaml খোঁজে। Base file-এর পাশে override file থাকলে Compose সেটিও নিজে থেকেই লোড করে।
ls compose.yaml compose.override.yaml
docker compose up -dদুটি file উপস্থিত থাকলে, ফলাফলটি file দুটির নাম হাতে টাইপ করে দেওয়ার সমান।
docker compose -f compose.yaml -f compose.override.yaml up -dCompose যে নামগুলো শনাক্ত করে সেগুলো হলো compose.override.yaml, compose.override.yml এবং পুরোনো docker-compose.override.yml ও docker-compose.override.yaml। অন্য কোনো নাম, যেমন compose.dev.yaml, -f দিয়ে নির্দিষ্ট করে দিলেই কেবল লোড হয়।
আপনি একটি -f pass করলেই automatic loading বন্ধ হয়। docker compose -f compose.yaml up ঠিক সেই একটি file পড়ে এবং override file উপেক্ষা করে। এই বৈশিষ্ট্যের ওপরই এই guide-এর পরবর্তী dev ও prod pattern নির্ভর করে।
Server-এ এর সুবিধা ও ঝুঁকি দুটোই আছে। Deploy directory-তে পড়ে থাকা override file ওই directory থেকে চালানো প্রতিটি সাধারণ docker compose command-এ লোড হবে, cron job যে command চালায় সেটিতেও। এর ফলে production stack এমন একটি source directory bind-mount করতে পারে, যেটি কেউ ship করার কথা ভাবেনি। প্রতিটি deploy-এর পরে docker compose config চালান এবং ফলাফল পরীক্ষা করুন।
-f দিয়ে ক্রম নির্ধারণ এবং আপেক্ষিক path কোথায় নির্ধারিত হয়
Compose আপনি যে ক্রমে file সরবরাহ করেন, সেই ক্রমে configuration তৈরি করে। পরবর্তী file-গুলো আগের file-গুলোর মান পরিবর্তন করে এবং নতুন মান যোগ করে। বাম থেকে ডানে, সর্বশেষ মান কার্যকর হয়।
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dওই project-এর প্রতিটি command-এ একই file list প্রয়োজন। দুটি file দিয়ে up এবং একটি file দিয়ে logs চালালে আপনি ভিন্ন merged model ব্যবহার করছেন। এর ফলে Compose যে service-এর অস্তিত্ব অস্বীকার করে, সেই service পাওয়ার সম্ভাবনা তৈরি হয়। এর পরিবর্তে COMPOSE_FILE environment variable ব্যবহার করে list-টি একবার নির্ধারণ করুন।
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dLinux-এ separator হলো :, আর COMPOSE_PATH_SEPARATOR এটি পরিবর্তন করে। COMPOSE_FILE project-এর .env file-এও রাখা যায়। এতে এটি আপনার shell history-এর অংশ না হয়ে checkout-এর অংশ হয়। command line-এ সরাসরি নির্ধারিত যেকোনো মান environment variable-কে অগ্রাহ্য করে।
এখন সেই নিয়মটি দেখুন, যা bind mount নষ্ট করে। -f দিয়ে একাধিক file ব্যবহার করলে, সব file-এর সব আপেক্ষিক path প্রথম file-এর directory-র ভিত্তিতে নির্ধারিত হয়। যে file-এ path লেখা আছে, তার directory-র ভিত্তিতে নয়। deploy/prod/compose.prod.yaml-এর মধ্যে ./data:/var/lib/postgresql/data লিখলেও Compose base file-এর পাশে ./data খোঁজে। এরপর Docker ওই ভুল path-এ একটি খালি directory তৈরি করে। Container-টি তখন তার মধ্যে কোনো data ছাড়াই চালু হয়। এটি data loss-এর মতো দেখায়, কিন্তু প্রকৃত data loss হয় না। Base path নিজে নির্ধারণ করতে --project-directory ব্যবহার করুন। অথবা include ব্যবহার করুন, যা প্রতিটি file-এর নিজস্ব directory-র ভিত্তিতে path নির্ধারণ করে।
Project name একই base directory থেকে আসে। তাই কোন file-টি প্রথমে থাকবে তা পরিবর্তন করলে project-এর নামও পরিবর্তিত হতে পারে। Project-এর নাম পরিবর্তিত হলে নতুন container name এবং নতুন volume name তৈরি হয়। পুরোনো volume পুরোনো নামের অধীনে disk-এ থেকে যায়। Base file-এ top-level name: ব্যবহার করে নামটি নির্দিষ্ট করে দিন।
name: myappকোন ফিল্ড একীভূত হয় এবং কোনগুলো প্রতিস্থাপিত হয়
Compose ফিল্ডের নাম অনুযায়ী নয়, মানের ধরন অনুযায়ী একীভূত করে।
- একক-মানের ফিল্ড প্রতিস্থাপিত হয়।
image,command,entrypointএবংmem_limitপরের মানটি সরাসরি গ্রহণ করে।command-এ একটি আর্গুমেন্ট যোগ করা যায় না, কারণ override পুরো লাইনটি পুনর্লিখে দেয়। - Mapping key অনুযায়ী একীভূত হয়।
environment,labels,volumesএবংdevicesউভয় ফাইলের প্রতিটি key ধরে রাখে। উভয় ফাইলে একই key থাকলে পরের ফাইলের মান কার্যকর হয়।environmentএবংlabels-এর ক্ষেত্রে key হলো variable বা label-এর নাম।volumesএবংdevices-এর ক্ষেত্রে key হলো container path। - Sequence যুক্ত হয়।
dns,dns_search,expose,tmpfsএবংexternal_linksএকত্রিত হয়।expose: ["3000"]থাকা একটি base-এর সঙ্গে["4000", "5000"]থাকা একটি override একীভূত করলে ফলাফল হয়["3000", "4000", "5000"]।
চারটি sequence-এ একটি identity key থাকে। তাই সেই key-তে মিল থাকা entry যুক্ত হওয়ার পরিবর্তে একীভূত হয়। volumes, secrets এবং configs target-এর ভিত্তিতে মেলে। ports ip, target, published এবং protocol-এর সমন্বয়ের ভিত্তিতে মেলে।
ports নিয়মটি দুবার পড়ুন, কারণ এখানেই ভুল হওয়ার সম্ভাবনা বেশি। দুটি port entry-কে একই entry হিসেবে গণ্য করা হয় কেবল তখনই, যখন ওই চারটি অংশের সবকটি মেলে। যেকোনো একটি অংশ পরিবর্তন করলে Compose এটিকে দ্বিতীয়, সম্পর্কহীন port হিসেবে দেখে। তাই উভয় entry-ই রাখা হয়।
override করার পরেও আপনার port কেন প্রকাশিত থাকে
প্রতিটি interface-এ একটি service প্রকাশ করে এমন base file:
services:
web:
image: nginx:1.27
ports:
- "8080:80"শুধু localhost-এ bind করার জন্য লেখা একটি override। কারণ এর সামনে একটি reverse proxy থাকবে:
services:
web:
ports:
- "127.0.0.1:8080:80"কাজ হয়েছে ধরে নেওয়ার আগে ফলাফল পরীক্ষা করুন।
docker compose -f compose.yaml -f compose.prod.yaml configউভয় entry-ই output-এ আছে। ip অংশটি আলাদা: 0.0.0.0 বনাম 127.0.0.1। তাই merge-এর দৃষ্টিতে এগুলো দুটি আলাদা port, এবং আপনি যে public binding সরানোর চেষ্টা করেছিলেন তা এখনও model-এ আছে। Docker-এর ক্ষেত্রে এটি আরও গুরুত্বপূর্ণ, কারণ প্রকাশিত port আপনার firewall rules-এর আগে iptables-এ লেখা হয়। এই প্রক্রিয়াটি কেন প্রকাশিত Docker port ufw-এর নিয়ম অতিক্রম করে অংশে ব্যাখ্যা করা হয়েছে।
এর দুটি সমাধান আছে। সরাসরি সমাধান হলো !override tag। এটি সম্পূর্ণ attribute প্রতিস্থাপন করে এবং merge rules এড়িয়ে যায়:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override ব্যবহারের জন্য Compose v2.24.4 বা পরবর্তী সংস্করণ প্রয়োজন। Portable সমাধানে কোনো tag প্রয়োজন নেই: base file থেকে ports সম্পূর্ণ বাদ দিন এবং এটি শুধু environment-specific file-গুলোতে declare করুন। Merge করার মতো কিছু না থাকলে অনিচ্ছাকৃতভাবে প্রকাশ পাওয়ার মতোও কিছু থাকে না। নিচের worked example-এ এই pattern ব্যবহার করা হয়েছে।
বেস ফাইলের সেট করা কোনো মান মুছে ফেলা
!reset একটি attribute সরিয়ে সেটিকে ডিফল্ট মানে বা null-এ ফিরিয়ে দেয়। এটি একটি মান গ্রহণ করে এবং তা উপেক্ষা করে। তাই বৈধ কিন্তু খালি কিছু লিখুন।
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset-এর জন্য Compose v2.24 বা পরবর্তী সংস্করণ প্রয়োজন। বেস ফাইলটি সম্পাদনা করার অধিকার আপনার না থাকলে এটি ব্যবহার করুন, যেমন আপনি কোনো vendor fragment অন্তর্ভুক্ত করলে।
অংশ দিয়ে একত্র করা stack-এর জন্য include
include আপনার model-এ আরেকটি Compose application যুক্ত করে। এটি top-level element, flag নয়।
include:
- path: ../commons/compose.yamlinclude-এর প্রতিটি path নিজস্ব project directory-সহ আলাদা Compose application model হিসেবে লোড হয়। তাই ওই file-এর ভেতরের relative path সেই file-এর নিজস্ব directory-এর ভিত্তিতে resolve হয়। এটিই -f-এর সঙ্গে প্রকৃত পার্থক্য। fragment অন্য folder বা অন্য repository-তে থাকলে include ব্যবহার করার কারণও এটাই।
দীর্ঘ form-এ sub-option থাকে।
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath একটি list গ্রহণ করে। ওই file-গুলো স্বাভাবিক নিয়মে একসঙ্গে merge হয়। এরপর ফলাফল আপনার model-এ যুক্ত হয়। project_directory included file-এর relative path resolve করার জন্য ব্যবহৃত base path নির্ধারণ করে। env_file included file-এর interpolation-এর জন্য নিজস্ব variables নির্ধারণ করে। এতে shared fragment নীরবে আপনার project-এর .env পড়তে পারে না। include ব্যবহারের জন্য Compose v2.20.0 বা পরবর্তী version প্রয়োজন।
আপনার file এবং included file-এর মধ্যে resource name একই হলে সেগুলো নীরবে merge না হয়ে error হিসেবে রিপোর্ট হয়। এটি ইচ্ছাকৃত আচরণ। Included file যে resource ঘোষণা করেছে, তাতে পরিবর্তন করতে হলে পরিবর্তনটি compose.override.yaml-এ লিখুন। Override assembled model-এর ওপর প্রয়োগ হয়। তাই resource-এর সঙ্গে সংঘর্ষ ছাড়াই included resource পরিবর্তন করা যায়।
সংক্ষেপে: include আলাদা application-গুলোকে একত্র করে, আর -f একটি application-এর ওপর configuration-এর স্তর যোগ করে।
একটি VPS-এ dev এবং prod বিভাজন
তিনটি ফাইলে পুরো প্যাটার্নটি দেখানো হলো। base ফাইলে সর্বত্র প্রযোজ্য সেটিংস ঘোষণা করা হয়, এবং এতে কোনো port প্রকাশ করা হয় না।
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 শর্তের কারণে app এমন database-এর জন্য অপেক্ষা করে, যা সাড়া দিচ্ছে; শুধু বিদ্যমান কোনো container-এর জন্য নয়। এটি healthcheck এবং depends_on শর্তাবলি-এ ব্যাখ্যা করা হয়েছে। POSTGRES_PASSWORD project-এর .env file থেকে interpolated হয়, যা কখনো git-এ রাখা হয় না। নিরাপদ বিকল্পগুলোর জন্য env file এবং Compose secret দেখুন।
এরপর compose.override.yaml, যা Compose নিজে থেকে load করে। এটি developer-এর file।
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"laptop-এ bare docker compose up ব্যবহার করলে এই দুটি file merge হয়। command image-এর default প্রতিস্থাপন করে, কারণ এটি single-valued। LOG_LEVEL, info-কে প্রতিস্থাপন করে, কারণ environment key অনুযায়ী merge হয়। bind mount এবং প্রকাশিত দুটি port সরাসরি যোগ হয়। Database port-টি localhost-এ bind করা থাকে, তাই shared network-এ থাকা কোনো laptop PostgreSQL-কে একই network-এর অন্যদের জন্য উন্মুক্ত করে না।
সবশেষে, compose.prod.yaml। এর নাম Compose নিজে থেকে খোঁজে না, তাই এটি ভুল করে load হয় না।
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MVPS-এ আপনি দুটি file-এর নাম উল্লেখ করবেন। নাম উল্লেখ করার এই কাজটিই 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-এ দুটি service-ই running হিসেবে দেখানো উচিত, এবং db-এ (healthy) দেখা উচিত। আপনি -f পাস করেছেন, তাই compose.override.yaml পড়া হয়নি। ফলে file-টি একই directory-তে থাকলেও dev command, source bind mount এবং public port 3000 production-এ পৌঁছাতে পারে না। Port 8000 শুধু localhost-এ চালু আছে, proxy-এর জন্য প্রস্তুত। দ্বিতীয় service যোগ করার সময় Traefik-এর পেছনে একাধিক app চালানো দেখুন।
Server-এর .env-এ COMPOSE_FILE=compose.yaml:compose.prod.yaml সেট করুন। এরপর আপনার বাকি command-গুলো আবার সরাসরি docker compose logs -f app হয়ে যাবে।
স্থাপনের আগে একীভূত মডেল পড়ুন
docker compose config সম্পূর্ণ একীভূত এবং সম্পূর্ণ interpolated মডেলটি প্রিন্ট করে। এটি কোনো preview নয়। এটিই Compose যে সঠিক input-এর ভিত্তিতে কাজ করবে। তাই output আপনার প্রত্যাশার সঙ্গে না মিললে output-ই সঠিক।
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}-কে unexpanded রাখে। কোথাও output paste করার আগে এটি ব্যবহার করুন, কারণ সাধারণ config প্রতিটি resolved secret-এর clear text প্রিন্ট করে। --services শুধু service-এর নাম তালিকাভুক্ত করে। কোনো include প্রত্যাশিত জিনিসটি pull করেছে কি না নিশ্চিত করার এটি একটি দ্রুত উপায়।
ব্যর্থতার ধরন এবং আপনি যা দেখতে পাবেন
no configuration file provided: not found। Compose পড়ার মতো কিছু খুঁজে পায়নি। আপনি প্রজেক্ট ডিরেক্টরির বাইরে আছেন, অথবা COMPOSE_FILE এমন একটি path উল্লেখ করে যা বিদ্যমান নয়। Compose ডিফল্ট base file খুঁজতে parent directory-গুলোতে অনুসন্ধান করে, কিন্তু আপনি নিজে নাম দেওয়া file-এর জন্য কোথাও অনুসন্ধান করে না।
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. Interpolation প্রজেক্টের .env file এবং shell environment-এর ভিত্তিতে মান নির্ধারণ করে। এখানে project directory হলো প্রথম -f file-এর directory। .env থাকা directory থেকে ভিন্ন কোনো directory-তে deploy করলে এই সতর্কবার্তা দেখা যায়। এরপর এমন একটি database তৈরি হয়, যা কোনো connection গ্রহণ করে না।
আপনার override edit docker compose config-তে দেখা যাচ্ছে না। আপনি হয় -f দিয়েছেন, যার ফলে automatic override loading বন্ধ থাকে, অথবা Compose parent directory-তে compose.yaml খুঁজে পেয়েছে এবং আপনার override file সেটির পাশে নেই। অন্য কোনো argument ছাড়া docker compose config চালালে Compose আসলে কোন model তৈরি করছে তা দেখা যায়।
একটি bind mount খালি, এবং Docker এমন একটি directory তৈরি করেছে যা আপনি চাননি। Relative path-টি প্রথম file-এর directory-এর ভিত্তিতে নির্ধারিত হয়েছে। Path ঠিক করুন, --project-directory দিন, অথবা fragment-টি include-এর পরে সরিয়ে নিন।
Containers নতুন name নিয়ে ফিরে আসে এবং একটি volume খালি দেখায়। Project name পরিবর্তিত হয়েছে, কারণ project name প্রথম file-এর directory অনুসরণ করে। Base file-এ top-level name: যোগ করুন। এতে name আর পরিবর্তিত হবে না। পুরোনো volume এখনও পুরোনো prefix-এর অধীনে আছে, এবং docker volume ls এটি দেখাবে।
Override-এ সরিয়ে দেওয়া একটি port এখনও open। ports merge replace করার পরিবর্তে append করেছে। docker compose config দিয়ে নিশ্চিত করুন। এরপর !override ব্যবহার করুন, অথবা ports-কে base file-এর বাইরে সরিয়ে নিন।
FAQ
Compose কি স্বয়ংক্রিয়ভাবে compose.override.yaml লোড করে?
হ্যাঁ, যখন আপনি কোনো -f flag ছাড়া docker compose চালান। Compose কাজের directory এবং তার parent directory-গুলোতে compose.yaml অথবা docker-compose.yaml খোঁজে। কোনো override file এর পাশে থাকলে সেটি দ্বিতীয় ধাপে লোড হয়। স্বীকৃত নামগুলো হলো compose.override.yaml, compose.override.yml, docker-compose.override.yml এবং docker-compose.override.yaml। যেকোনো -f দিলে এই আচরণ বন্ধ হয়। তাই docker compose -f compose.yaml up শুধু একটি file পড়ে।
একাধিক -f file কোন ক্রমে merge হয়?
বাম থেকে ডানে। আপনি যে ক্রমে file দেন, Compose সেই ক্রমে configuration তৈরি করে। প্রতিটি file তার আগের file-গুলোর মান override করে এবং নতুন মান যোগ করে। তাই একই conflict থাকলে লাইনের শেষের file-টির মান কার্যকর হয়। ওই project-এর প্রতিটি command-এ একই তালিকা ব্যবহার করতে হবে। এই কাজের জন্যই COMPOSE_FILE=compose.yaml:compose.prod.yaml ব্যবহৃত হয়।
override করার পরও port কেন published থাকে?
কারণ ports entry-গুলো ip, target, published এবং protocol-এর সম্পূর্ণ সমষ্টি দিয়ে শনাক্ত হয়। 8080:80 base-এর ওপর 127.0.0.1:8080:80 override করলে ip অংশে পার্থক্য থাকে। তাই Compose এটিকে দ্বিতীয় port হিসেবে গণ্য করে এবং দুটিই রাখে। docker compose config চালালে আপনি দুটি entry দেখতে পাবেন। Compose v2.24.4 বা পরের সংস্করণে ports: !override ব্যবহার করুন। অথবা base file থেকে ports বাদ দিন, যাতে merge করার মতো কোনো মান না থাকে।
include এবং -f-এর মধ্যে পার্থক্য কী?
-f কয়েকটি file-কে একটি application-এর ওপর স্তর হিসেবে যুক্ত করে। প্রতিটি file-এর relative path প্রথম file-এর directory অনুযায়ী resolve হয়। include একটি পৃথক Compose application যুক্ত করে। এতে অন্তর্ভুক্ত প্রতিটি path তার নিজস্ব project directory বজায় রাখে, তাই তার relative path নিজস্ব directory অনুযায়ী resolve হয়। নিজের stack-এর environment layer-এর জন্য -f ব্যবহার করুন। অন্যত্র রক্ষণাবেক্ষণ করা fragment-এর জন্য include ব্যবহার করুন। include-এর জন্য Compose v2.20.0 বা পরের সংস্করণ প্রয়োজন।
base file-এ নির্ধারিত কোনো value কীভাবে সরাব?
Compose v2.24 বা পরের সংস্করণে !reset tag ব্যবহার করুন। overriding file-এ ports: !reset [] অথবা MY_VAR: !reset null লিখলে attribute-টি তার default মানে অথবা null-এ ফিরে যায়। tag-এ দেওয়া value প্রয়োজনীয়, তবে তা উপেক্ষা করা হয়। কোনো attribute মুছে না দিয়ে প্রতিস্থাপন করতে চাইলে !override ব্যবহার করুন। এর জন্য v2.24.4 বা পরের সংস্করণ প্রয়োজন।