Khi dùng Docker Compose cho nhiều môi trường (dev, staging, prod), bạn sẽ gặp một vấn đề quen thuộc: 80% config giống nhau giữa các môi trường, chỉ khác vài dòng environment, volume mount, hoặc resource limits. Copy-paste config qua nhiều file dẫn đến drift – dev với staging khác nhau, lỗi chỉ xuất hiện khi deploy.
Compose có profiles, extends, include để giải quyết chính xác vấn đề này: chia sẻ config chung, chỉ override phần khác biệt, không duplicate, không sai lệch giữa các môi trường.
Profiles – bật/tắt service theo ngữ cảnh
Không phải service nào cũng cần chạy mọi lúc. Dev cần Adminer để xem DB, staging cần mock SMTP server, prod không cần cả hai. Pre-Compose v2, cách phổ biến là tạo file riêng cho từng tổ hợp service. Compose profiles giải quyết gọn hơn nhiều.
Định nghĩa service với profiles:
# docker-compose.yml
services:
app:
build: .
ports: ["3000:3000"]
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
adminer: # chỉ dùng khi debug DB
image: adminer:latest
profiles: [debug]
ports: ["8080:8080"]
mailpit: # chỉ dùng local/dev
image: axllent/mailpit
profiles: [tools]
ports: ["8025:8025"]
k6: # chỉ dùng khi load test
image: grafana/k6
profiles: [perf]
entrypoint: ["k6", "run", "/scripts/load-test.js"]
volumes: ["./tests:/scripts"]
Mặc định, docker compose up chỉ start service không có profiles – tức là app và postgres. Muốn kèm thêm service trong profile:
# Dev bình thường + adminer + mailpit
docker compose --profile debug --profile tools up -d
# Load test
docker compose --profile perf run k6
# Kết hợp shorthand: chạy service thuộc profile "debug"
docker compose --profile debug up -d
Profile hoạt động thế nào?
Compose profiles là tính năng compile-time – khi parse file YAML, Compose kiểm tra service nào có profiles set. Nếu không có --profile flag trùng khớp, service đó bị loại khỏi dependency graph. Nó giống #ifdef trong C hơn là feature flag runtime.
--profile tools, CI chạy --profile ci, prod không cần profile nào.Lưu ý về depends_on và profiles
Service không có profile không thể depends_on service có profile (vì service có profile có thể không tồn tại khi không bật profile). Giải pháp: dùng required: false (Compose v2.24+):
services:
app:
depends_on:
adminer:
required: false # app vẫn start dù adminer không được bật
Hoặc tách biệt: service core không nên phụ thuộc vào service optional.
Extends – kế thừa service definition
Đây là tính năng mình cần nhất: định nghĩa service một lần, override phần khác biệt cho từng môi trường.
Pattern cơ bản
Tạo file common.yml chứa base service:
# common.yml
services:
app-base:
image: myapp:latest
environment:
NODE_ENV: production
LOG_LEVEL: info
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
retries: 3
deploy:
resources:
limits:
memory: 512M
File dev override:
# docker-compose.dev.yml
services:
app:
extends:
file: common.yml
service: app-base
build: . # override: dev build từ source
environment:
NODE_ENV: development # override: dev mode
LOG_LEVEL: debug
volumes:
- .:/app # thêm: hot reload
- /app/node_modules
ports:
- "3000:3000"
deploy: # override: dev không cần limit
resources:
limits:
memory: 256M
File prod:
# docker-compose.prod.yml
services:
app:
extends:
file: common.yml
service: app-base
image: registry.example.com/myapp:${VERSION} # override: pinned version
environment:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL}
secrets:
- db_password
deploy:
resources:
limits:
memory: 1G
Dùng:
docker compose -f docker-compose.dev.yml up -d
docker compose -f docker-compose.prod.yml up -d
Merge strategy
Khi extend, Compose merge field theo kiểu shallow merge cho map, replace cho scalar và array. Cụ thể:
| Field type | Hành vi |
|---|---|
Scalar (image, command) | Override hoàn toàn |
Array (ports, volumes) | Override hoàn toàn (không merge) |
Map (environment, labels, deploy) | Merge: key mới thêm vào, key trùng override |
Điều này có nghĩa nếu base định nghĩa ports: ["3000:3000"], dev thêm ports: ["9229:9229"] là bạn mất port 3000. Muốn merge, phải liệt kê lại toàn bộ.
ports, volumes, dns, cap_add đều bị replace chứ không append. Nếu base có 3 volumes, dev thêm 1 volume mới, bạn phải copy cả 3 volumes từ base sang dev. Đây là một trong những footgun phổ biến nhất của extends.Extends nhiều cấp
Có thể chain extends – service extend từ base, base extend từ base-of-base. Nhưng mình khuyên không nên quá 2 cấp vì khó debug. Khi lỗi merge, dùng docker compose config để xem config thực tế sau khi resolve toàn bộ extends:
docker compose -f docker-compose.dev.yml config > resolved.yml
Include – ghép nhiều file Compose
include là tính năng mới từ Compose v2.20+. Khác với extends (kế thừa từng service), include nhúng toàn bộ một Compose project khác vào project hiện tại. Service từ project được include chạy như thể nó được định nghĩa tại chỗ.
Use case: mono-repo shared infrastructure
Giả sử cấu trúc repo:
monorepo/
├── shared/
│ └── infra.yml # PostgreSQL, Redis, NGINX shared
├── service-a/
│ └── docker-compose.yml
├── service-b/
│ └── docker-compose.yml
└── docker-compose.yml # root: include tất cả
shared/infra.yml:
services:
postgres:
image: postgres:16-alpine
volumes: [pgdata:/var/lib/postgresql/data]
redis:
image: redis:7-alpine
volumes:
pgdata:
service-a/docker-compose.yml:
include:
- path: ../shared/infra.yml
services:
api-a:
build: .
depends_on: [postgres, redis]
ports: ["3001:3001"]
Root docker-compose.yml:
include:
- path: service-a/docker-compose.yml
- path: service-b/docker-compose.yml
Một lệnh duy nhất:
docker compose up -d
Kết quả: postgres, redis, api-a, api-b cùng chạy, network chung, volume chung.
Include vs Extends
extends | include | |
|---|---|---|
| Scope | Từng service riêng lẻ | Toàn bộ Compose file |
| Merge | Kế thừa + override field | Nhúng project như một unit |
| Network | Service extend nằm trong network của file gọi | Service từ file include tạo network riêng (cần explicit join) |
| Dùng khi | Cùng service, khác config theo env | Nhiều service độc lập, cần chạy cùng nhau |
| Compose version | Từ v1 (legacy) | Từ v2.20+ |
extends cho dev/prod parity của cùng một service. include cho ghép shared infrastructure (DB, message queue, monitoring stack) vào project service cụ thể. Có thể dùng cả hai cùng lúc.Secrets management – không env var cho password
Env var để truyền config là fine. Env var để truyền password là antipattern. Ai có quyền docker inspect hoặc đọc /proc/<pid>/environ đều thấy được. Compose hỗ trợ secrets: top-level element để mount secret an toàn hơn.
Secrets trong Compose
# docker-compose.yml
secrets:
db_password:
file: ./secrets/db_password.txt # file chứa plaintext secret (chỉ tồn tại trên máy dev/CI)
api_key:
environment: API_KEY # lấy từ env var của host, ghi vào file secret
services:
app:
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password # app đọc từ file, không từ env var
Secret được mount vào /run/secrets/<secret_name> dưới dạng file readonly. App của bạn đọc secret từ file:
// Node.js
const fs = require("fs");
const dbPassword = fs
.readFileSync(
process.env.DB_PASSWORD_FILE || "/run/secrets/db_password",
"utf8"
)
.trim();
# Python
from pathlib import Path
db_password = Path("/run/secrets/db_password").read_text().strip()
BuildKit secrets (build-time)
Build-time cũng có secrets riêng, không nằm trong image layer:
# Dockerfile
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
export NPM_TOKEN=...
docker compose build --set *.args.NPM_TOKEN=$NPM_TOKEN
File .npmrc có thể reference token từ env mà không leak vào image layer cuối cùng.
docker inspect nhưng không cần full KMS.Compose v5 – ecosystem thay đổi
Tháng 7/2025, Docker chính thức end-of-life Compose v1 (Python). Compose v2 (Go, plugin của Docker CLI) trở thành bản duy nhất được maintain. Cùng lúc, Compose Specification đạt version 5 với vài thay đổi đáng chú ý:
Compose v2 Go SDK
// Go SDK cho phép nhúng Compose logic vào tooling của team
import "github.com/compose-spec/compose-go/v2/types"
project, _ := loader.Load(types.ConfigDetails{
WorkingDir: ".",
ConfigFiles: []types.ConfigFile{
{Filename: "docker-compose.yml"},
},
})
// project.Services, project.Networks, project.Volumes...
SDK này giúp viết tool kiểm tra config, generate manifest cho orchestrator khác, hoặc tự động validate Compose file trong CI.
OCI artifact support
Compose v5 cho phép đóng gói toàn bộ Compose application thành OCI artifact và push lên registry:
# Pack
docker compose pack myapp:1.0
# Push lên registry như một artifact
docker push registry.example.com/myapp:1.0
# Pull và deploy từ xa
docker compose deploy registry.example.com/myapp:1.0
Không cần checkout repo, không cần clone source – kéo artifact từ registry là chạy được. Hữu ích cho GitOps pipeline và air-gapped environment.
Compose hooks
services:
app:
image: myapp:latest
hooks:
post-start:
- command: |
curl -s http://localhost:3000/health || exit 1
pre-stop:
- command: |
echo "Draining connections..."
sleep 5
Hooks chạy command trên host (không phải trong container) tại các lifecycle event: pre-start, post-start, pre-stop, post-stop. Dùng để health check before marking ready, graceful drain, hoặc gửi notification.
podman-compose, nerdctl compose, và Compose support trong AWS ECS, Azure ACI. Compose v5 spec chuẩn hóa những thứ trước đây là Docker-specific.Dev/Staging/Prod parity với multiple Compose files
Đây là pattern mình dùng hàng ngày. Thay vì 3 file riêng biệt với 80% duplicate, dùng base + override:
project/
├── docker-compose.yml # base: common config cho mọi env
├── docker-compose.dev.yml # override: dev-specific
├── docker-compose.staging.yml # override: staging-specific
└── docker-compose.prod.yml # override: production-specific
Base file
# docker-compose.yml (base)
services:
app:
image: myapp:${TAG:-latest}
environment:
LOG_LEVEL: info
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:16-alpine
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
volumes:
pgdata:
Dev override
# docker-compose.dev.yml
services:
app:
build: . # build từ source, không pull image
environment:
LOG_LEVEL: debug
NODE_ENV: development
volumes:
- .:/app # hot reload
- /app/node_modules
ports: ["3000:3000"]
profiles: [] # explicit: không profile
postgres:
ports: ["5432:5432"] # expose cho DB tool local
Staging override
# docker-compose.staging.yml
services:
app:
image: registry.example.com/myapp:staging-${CI_COMMIT_SHA}
environment:
LOG_LEVEL: info
NODE_ENV: staging
deploy:
resources:
limits:
memory: 512M
postgres:
environment:
POSTGRES_PASSWORD: ${STAGING_DB_PASSWORD}
deploy:
resources:
limits:
memory: 256M
Production override
# docker-compose.prod.yml
services:
app:
image: registry.example.com/myapp:${VERSION}
environment:
LOG_LEVEL: warn
NODE_ENV: production
secrets: [db_password, api_key]
deploy:
resources:
limits:
memory: 1G
cpus: "2"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
postgres:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
deploy:
resources:
limits:
memory: 1G
secrets:
db_password:
external: true # secret được tạo trước: `docker secret create ...`
api_key:
external: true
Cách dùng
# Dev
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Khi chỉ định nhiều -f flag, Compose merge theo thứ tự: file sau override file trước. Base file được load đầu tiên, override file sau.
Hoặc dùng biến môi trường:
# .env.dev
COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml
# .env.prod
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml
Kiểm tra merge
# Xem config thực tế sau merge
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
# So sánh diff giữa các env
diff <(docker compose -f docker-compose.yml -f docker-compose.dev.yml config) \
<(docker compose -f docker-compose.yml -f docker-compose.prod.yml config)
Mermaid diagram thể hiện cấu trúc merge:
flowchart TB
subgraph "Base + Override Pattern"
BASE[docker-compose.yml<br/>Base: services, networks,<br/>volumes, healthcheck]
end
subgraph "Environment Overrides"
DEV[docker-compose.dev.yml<br/>build từ source<br/>hot reload volumes<br/>debug logging]
STG[docker-compose.staging.yml<br/>image từ registry<br/>resource limits 512M<br/>CI-specific env]
PRD[docker-compose.prod.yml<br/>pinned version<br/>resource limits 1G<br/>secrets, log rotation]
end
subgraph "Runtime Merge"
RES_DEV((DEV runtime)):::dev
RES_STG((STAGING runtime)):::stg
RES_PRD((PRODUCTION runtime)):::prod
end
BASE --> RES_DEV
BASE --> RES_STG
BASE --> RES_PRD
DEV --> RES_DEV
STG --> RES_STG
PRD --> RES_PRD
classDef dev fill:#4a9,stroke:#333;
classDef stg fill:#da2,stroke:#333;
classDef prod fill:#d44,stroke:#333;
Compose trong CI/CD
Compose không chỉ để chạy local – nó là công cụ mạnh cho integration test trong CI pipeline.
Pattern: test service với Compose
# docker-compose.ci.yml
services:
postgres-test:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
retries: 5
tmpfs: ["/var/lib/postgresql/data"] # in-memory, sạch sau test
redis-test:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
tests:
build:
context: .
target: test # multi-stage build: test stage
environment:
DATABASE_URL: postgresql://postgres:test@postgres-test:5432/testdb
REDIS_URL: redis://redis-test:6379
depends_on:
postgres-test:
condition: service_healthy
redis-test:
condition: service_healthy
volumes:
- ./coverage:/app/coverage
command: ["npm", "test", "--", "--coverage"]
CI script:
# Build test image
docker compose -f docker-compose.ci.yml build tests
# Run tests, tự động stop sau khi tests exit
docker compose -f docker-compose.ci.yml up \
--abort-on-container-exit \
--exit-code-from tests
# Cleanup
docker compose -f docker-compose.ci.yml down -v
--abort-on-container-exit: Dừng tất cả container khi bất kỳ container nào exit (không phải chỉ tests).--exit-code-from tests: Trả về exit code của servicetests– pipeline pass/fail dựa trên kết quả test.tmpfscho postgres data: DB trong memory, sạch hoàn toàn sau test, không cần cleanup volume.
--exit-code-from kết hợp với multi-stage Dockerfile. Stage test chứa devDependencies (Jest, Mocha, pytest…), stage production chỉ có production dependencies. Image test nặng nhưng không ảnh hưởng image production.Healthcheck trong Compose – depends_on với condition
Đã nói kỹ về HEALTHCHECK trong Phần 8: Healthcheck & autoheal pattern, nhưng phần Compose-specific đáng nhắc lại.
condition: service_healthy
services:
app:
depends_on:
postgres:
condition: service_healthy # đợi postgres healthy thực sự
restart: true # restart app nếu postgres restart
redis:
condition: service_healthy
Không có condition, depends_on chỉ đợi container started – process chạy, chưa chắc DB đã sẵn sàng nhận connection. Với condition: service_healthy, Compose đợi healthcheck pass.
--wait flag
docker compose up -d --wait
Flag --wait (Compose v2.17+) block cho đến khi tất cả service có healthcheck đạt healthy. Hữu ích trong script và CI:
#!/bin/bash
docker compose up -d --wait
# Lúc này đảm bảo app đã sẵn sàng
curl -f http://localhost:3000/health
--wait yêu cầu mọi service có healthcheck. Service không có healthcheck sẽ được coi là “ready” ngay lập tức. Nếu bạn dùng --wait trong CI, đảm bảo tất cả service dependency (postgres, redis, etc.) đều có healthcheck defined.Tổng kết
Docker Compose không chỉ là công cụ chạy docker compose up cho dev. Khi team phình ra, số lượng service và environment tăng, bạn cần một chiến lược quản lý config:
| Vấn đề | Giải pháp |
|---|---|
| Service optional theo ngữ cảnh | Profiles – gắn profiles: [debug, perf] |
| Duplicate config giữa các env | Extends – kế thừa base, override field khác biệt |
| Nhiều Compose project cần chạy chung | Include – ghép shared infra vào project |
| Password trong env var | Secrets – mount file vào /run/secrets/ |
| Dev/Staging/Prod drift | Multiple Compose files – base + override per env |
| Test trong CI với service thật | Compose CI pattern – --exit-code-from + tmpfs |
| Race condition service khởi động | depends_on: condition: service_healthy + --wait |
Pattern “base + override” với COMPOSE_FILE env var là thứ mình setup cho mọi dự án từ ngày biết đến nó. Không còn copy-paste, không còn sai lệch config giữa các môi trường, và quan trọng nhất: code review chỉ cần check phần override – phần base đã được test kỹ.
Câu hỏi hay gặp
Khi nào dùng extends, khi nào dùng multiple -f files?
Extends khi bạn muốn tái sử dụng service definition của cùng một service qua các file khác nhau (base trong common.yml, các file dev/prod extend từ đó). Multiple -f files khi bạn có một base file hoàn chỉnh và muốn override một số field cho từng environment. Khác biệt chính: extends hoạt động ở cấp service, multiple files hoạt động ở cấp project. Với multiple files, bạn override bất kỳ top-level element nào (services, networks, volumes, secrets, configs).
Làm sao debug khi merge/extend không như mong đợi?
docker compose -f base.yml -f override.yml config > resolved.yml
Lệnh config resolve toàn bộ extends, include, variable substitution, và merge multiple files thành một file duy nhất. Kiểm tra resolved.yml để thấy chính xác config Compose sẽ dùng. Đây là lệnh đầu tiên mình chạy mỗi khi “nó không hoạt động như tôi nghĩ.”
Compose profiles có hoạt động với extends/include không?
Có. Profile của service trong file được include/extend vẫn được tôn trọng. Nếu common.yml định nghĩa service với profiles: [debug], khi bạn extend service đó, profile vẫn được giữ nguyên (trừ khi bạn override profiles field). docker compose --profile debug up sẽ bật service đó.
Compose v5 có phá vỡ compose file hiện tại không?
Gần như không. Compose Specification v5 tương thích ngược với v3.x. Cú pháp YAML không thay đổi về cơ bản – thay đổi lớn nhất là ecosystem (Go SDK, OCI artifact, hooks) và các tính năng mới được thêm vào (không phải thay đổi cú pháp cũ). File docker-compose.yml viết cho v3 vẫn chạy tốt trên Compose v2 engine với spec v5.
Bài trước: Phần 9: Docker Compose cơ bản – multi-container orchestration, networking, volumes, và docker compose CLI.
Bài tiếp theo: Phần 11: Docker networking: hands-on debug – bridge, overlay, DNS, service discovery, và troubleshooting.