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à apppostgres. 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.

Pattern: “core + optional”. Định nghĩa service core (app, DB, cache) không có profile – chúng luôn chạy. Service optional (admin tool, mock service, debug container, load test client) gắn profile. Dev chạy --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 typeHà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ộ.

Extends không merge array. 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

extendsinclude
ScopeTừng service riêng lẻToàn bộ Compose file
MergeKế thừa + override fieldNhúng project như một unit
NetworkService extend nằm trong network của file gọiService từ file include tạo network riêng (cần explicit join)
Dùng khiCùng service, khác config theo envNhiều service độc lập, cần chạy cùng nhau
Compose versionTừ v1 (legacy)Từ v2.20+
Quy tắc của mình: 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.

Secret trong Compose file chỉ là “development-grade” security. Trên production thực sự, dùng HashiCorp Vault, AWS Secrets Manager, hoặc Kubernetes Secrets với encryption at rest. Compose secrets chủ yếu hữu ích cho dev/staging và CI/CD – nơi bạn muốn tránh lộ password trong 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.

Compose Specification vs Docker Compose. “Compose Specification” là spec mở do Docker + AWS + Microsoft + Red Hat cùng maintain trên compose-spec/compose-spec. “Docker Compose” là implementation chính thức của Docker. Các implementation khác gồm 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 service tests – pipeline pass/fail dựa trên kết quả test.
  • tmpfs cho postgres data: DB trong memory, sạch hoàn toàn sau test, không cần cleanup volume.
CI tip: Dùng --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ảnhProfiles – gắn profiles: [debug, perf]
Duplicate config giữa các envExtends – kế thừa base, override field khác biệt
Nhiều Compose project cần chạy chungInclude – ghép shared infra vào project
Password trong env varSecrets – mount file vào /run/secrets/
Dev/Staging/Prod driftMultiple Compose files – base + override per env
Test trong CI với service thậtCompose CI pattern--exit-code-from + tmpfs
Race condition service khởi độngdepends_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.