Docker for Developers · Part 6 — Compose in Depth: Env, Profiles, Healthchecks & Scaling
Level up Compose: env files and variable interpolation, healthchecks with depends_on condition, profiles for optional services, multiple compose files / overrides, and scaling services.
This is Part 6 of a 10-part series that takes you from “I’ve heard of Docker” to confidently building, running, and debugging containerized apps — Docker, Docker Compose, and Kubernetes {Đây là Phần 6 của series 10 bài đưa bạn từ “mới nghe nói về Docker” đến tự tin build, chạy và debug ứng dụng container — Docker, Docker Compose và Kubernetes}. Previous {Trước}: Part 5 — Docker Compose Fundamentals · Next {Tiếp}: Part 7 — Optimizing & Securing Images. Every part ends with exercises; do them, don’t just read {Mỗi phần kết thúc bằng bài tập; hãy làm, đừng chỉ đọc}.
In Part 5 you declared a multi-service stack in compose.yaml and brought it up with docker compose up {Trong Phần 5 bạn đã khai báo stack nhiều dịch vụ trong compose.yaml và bật bằng docker compose up}. That gets containers started — but “started” is not the same as ready {Điều đó chỉ làm container khởi động — “đã start” không đồng nghĩa sẵn sàng}. This part makes your Compose files robust and flexible: env interpolation, health-gated dependencies, optional profiles, file overrides, scaling, and restart policies {Phần này làm file Compose vững và linh hoạt: nội suy env, phụ thuộc theo health, profile tùy chọn, override file, scale và chính sách restart}.
Environment & variable interpolation {Biến môi trường & nội suy biến}
Compose reads a project .env file at the project root (same folder as compose.yaml) and uses those values when it parses the YAML — before containers start {Compose đọc file .env ở root project (cùng thư mục với compose.yaml) và dùng giá trị đó khi parse YAML — trước khi container chạy}. Syntax ${VAR} substitutes at parse time; ${VAR:-default} uses a default when unset {Cú pháp ${VAR} thay thế lúc parse; ${VAR:-default} dùng giá trị mặc định khi chưa set}.
# compose.yaml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
ports:
- "${DB_PORT:-5432}:5432"
# .env (project root — do NOT commit secrets; add .env to .gitignore)
POSTGRES_PASSWORD=devpass
DB_PORT=5433
env_file is different: it injects key/value pairs into the container’s environment at runtime — it does not interpolate the compose file itself {env_file khác: nó đưa cặp key/value vào môi trường container lúc runtime — không nội suy file compose}.
env_file: [.env.api] loads into the container; environment: NODE_ENV: ${NODE_ENV:-development} still interpolates in YAML {env_file nạp vào container; environment vẫn nội suy trong YAML}. Keep secrets out of images and git — .env locally, CI secrets in prod (Part 3) {Giữ secret ngoài image và git — .env local, secret CI trên prod (Phần 3)}.
Healthchecks & depends_on conditions {Healthcheck & điều kiện depends_on}
A container can be running while Postgres is still initializing — the classic race from Part 5’s short depends_on {Container có thể đang chạy trong khi Postgres vẫn đang khởi tạo — race kinh điển từ depends_on dạng ngắn ở Phần 5}. Fix it with a healthcheck on the dependency and condition: service_healthy on the consumer {Sửa bằng healthcheck trên dịch vụ phụ thuộc và condition: service_healthy trên dịch vụ cần chờ}.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -q"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
db:
condition: service_healthy
| Field | Role {Vai trò} |
|---|---|
test | Command that exits 0 = healthy {Lệnh exit 0 = healthy} |
interval | How often to run the test {Tần suất chạy test} |
timeout | Max wait per test run {Thời gian chờ tối đa mỗi lần test} |
retries | Failures before marking unhealthy {Số lần fail trước khi đánh unhealthy} |
start_period | Grace period — failures don’t count yet {Khoảng ân hạn — fail chưa tính} |
TIME ─────────────────────────────────────────────────────────────►
db container starts
│
├──── start_period (10s) ────┤ failures ignored here
│ │
│ pg_isready polls every interval
│ │
│ ▼
│ status: healthy
│ │
└────────────────────────────┼──► web may start (service_healthy)
│
without healthcheck: web starts here ──X── (DB may still be init)
docker compose up -d && docker compose ps # HEALTH: starting → healthy
Profiles — optional services {Profile — dịch vụ tùy chọn}
Not every developer needs Mailhog, Adminer, or a debug sidecar every day {Không phải ai cũng cần Mailhog, Adminer hay sidecar debug mỗi ngày}. Mark optional services with profiles — they stay off unless you opt in {Đánh dấu dịch vụ tùy chọn bằng profiles — chúng tắt trừ khi bạn bật}.
services:
api:
image: myapp:latest
# no profile → always starts with `docker compose up`
mailhog:
image: mailhog/mailhog
profiles: [debug]
ports:
- "8025:8025"
docker compose up -d # api only
docker compose --profile debug up -d # api + mailhog
Use profiles for dev-only tools, one-off importers, or heavy local dependencies you don’t want on every up {Dùng profile cho công cụ chỉ dev, job import một lần, hoặc dependency nặng không muốn mỗi lần up}.
Multiple compose files & overrides {Nhiều file compose & override}
Compose merges multiple files into one effective spec {Compose gộp nhiều file thành một spec hiệu lực}:
compose.yaml— your base stack {stack cơ sở}.compose.override.yaml— auto-loaded on the same machine for local tweaks (bind mounts, debug ports) {tự load trên cùng máy để chỉnh local (bind mount, cổng debug)}.-f— explicit extra files, e.g. prod overlay {file bổ sung tường minh, vd overlay prod}.
docker compose up -d
# equivalent to merging compose.yaml + compose.override.yaml if override exists
docker compose -f compose.yaml -f compose.prod.yaml up -d
Merge semantics (simplified) {Quy tắc gộp (rút gọn)}: later files override scalars and append lists where allowed; service names are the merge key — same name deep-merges keys {file sau ghi đè scalar và nối list khi được phép; tên service là khóa gộp — cùng tên thì merge sâu từng key}. Maps like environment and labels typically merge by key {map như environment, labels thường merge theo key}.
compose.override.yaml typically adds bind mounts and debug ports for api on your laptop {compose.override.yaml thường thêm bind mount và cổng debug cho api trên laptop}.
Scaling services {Scale dịch vụ}
Run multiple containers of the same service without duplicating YAML {Chạy nhiều container cùng một service mà không nhân đôi YAML}:
docker compose up -d --scale worker=3
docker compose ps # three worker containers
Good for stateless workers (queue consumers, batch jobs) on an internal network with no fixed host port per replica {Hợp worker stateless (consumer hàng đợi, batch) trên mạng nội bộ không gán cổng host cố định cho từng replica}. Bad when every replica maps the same ports: "8080:80" — only one can bind the host port {Không ổn khi mỗi replica cùng ports: "8080:80" — chỉ một bind được cổng host}. For HTTP scale-out on one host, put a reverse proxy or load balancer in front; in production clusters you usually scale in Kubernetes (Part 9+) instead {Để scale HTTP trên một host, đặt reverse proxy hoặc load balancer phía trước; trên cluster production thường scale bằng Kubernetes (Phần 9+) thay vì --scale}.
Restart policies {Chính sách restart}
When the daemon or host reboots, should Compose bring services back? {Khi daemon hoặc host reboot, Compose có tự bật lại dịch vụ?}
services:
api:
restart: unless-stopped # restart unless you explicitly stopped it
worker:
restart: on-failure
Common values: no (default), unless-stopped (good for local daemons), on-failure (workers) {Thường dùng: no, unless-stopped (daemon local), on-failure (worker)}.
Common pitfalls {Các bẫy thường gặp}
- Healthcheck command missing in the image —
pg_isreadymust exist in the Postgres image; a minimalFROM scratchapp won’t havecurlunless you install it {Lệnh healthcheck không có trong image —pg_isreadyphải có trong image Postgres; appFROM scratchkhông cócurltrừ khi bạn cài}. - Short
depends_ononly orders start —depends_on: [db]does not wait for healthy; usecondition: service_healthy{depends_ondạng ngắn chỉ sắp thứ tự start —depends_on: [db]không chờ healthy; dùngcondition: service_healthy}. .envvsenv_file—.envfeeds Compose interpolation;env_filesets container env. You can use both; don’t confuse which layer sees which variable {.envvsenv_file—.envcho nội suy Compose;env_fileset env container. Có thể dùng cả hai; đừng lẫn tầng nào thấy biến nào}.- Scaling a published service — duplicate host port bindings fail; scale internal workers, not
ports-mapped frontends {Scale dịch vụ đã publish cổng — bind cổng host trùng sẽ fail; scale worker nội bộ, không scale frontend cóports}. - Committing
.envwith secrets — add.envto.gitignore; commit.env.examplewith dummy values instead {Commit.envchứa secret — thêm.envvào.gitignore; commit.env.examplevới giá trị giả}.
Cheat sheet {Bảng tra nhanh}
# env & profiles
docker compose config # resolved YAML after interpolation
docker compose --profile debug up -d
# health
docker compose ps # HEALTH column
docker compose up -d --wait # wait for healthy (Compose v2.1+)
# overrides & scale
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose up -d --scale worker=3
# lifecycle
docker compose restart api
docker compose down -v
Patterns: healthcheck + depends_on: db: condition: service_healthy, profiles: [debug], restart: unless-stopped, KEY: ${KEY:-default} {Pattern: healthcheck, service_healthy, profile, restart, nội suy biến}.
Bài tập / Exercises
Work in compose-lab/ (mkdir -p compose-lab && cd compose-lab); clean up with docker compose down -v {Làm trong compose-lab/; dọn bằng docker compose down -v}. Exercises build on the same compose.yaml {Các bài mở rộng cùng compose.yaml}.
1. Add .env (POSTGRES_PASSWORD, WEB_PORT), interpolate in compose.yaml, confirm with docker compose config {Thêm .env, nội suy, xác nhận bằng docker compose config}.
Solution {Lời giải}
# .env
POSTGRES_PASSWORD=exercise
WEB_PORT=8080# compose.yaml — db + web
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
web:
image: nginx:alpine
ports: ["${WEB_PORT}:80"]docker compose config | grep -E 'POSTGRES_PASSWORD|"8080:80"'
docker compose up -d && curl -sI http://localhost:8080 | head -n 12. Add healthcheck on db and depends_on: db: condition: service_healthy on web; docker compose ps until db is healthy {Thêm healthcheck + service_healthy; docker compose ps đến khi db healthy}.
Solution {Lời giải}
# under db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -q"]
interval: 3s
timeout: 3s
retries: 10
start_period: 5s
# under web:
depends_on:
db: { condition: service_healthy }docker compose up -d && docker compose ps3. Add mailhog with profiles: [debug]; prove default up skips it {Thêm mailhog profile debug; chứng minh up mặc định bỏ qua}.
Solution {Lời giải}
mailhog:
image: mailhog/mailhog
profiles: [debug]
ports: ["8025:8025"]docker compose down && docker compose up -d && docker compose ps # no mailhog
docker compose --profile debug up -d && docker compose ps # mailhog up4. Add stateless worker (alpine, sleep infinity), --scale worker=3 {Thêm worker, scale 3}.
Solution {Lời giải}
worker:
image: alpine:3.20
command: sleep infinitydocker compose up -d --scale worker=3
docker compose ps | grep worker5. compose.override.yaml bind-mounts ./html → /usr/share/nginx/html; confirm merged page {Override bind-mount html/, xác nhận trang merge}.
Solution {Lời giải}
mkdir -p html && echo '<h1>override works</h1>' > html/index.html# compose.override.yaml
services:
web:
volumes: ["./html:/usr/share/nginx/html:ro"]docker compose up -d && curl -s http://localhost:8080/Stretch {Nâng cao}: compose.prod.yaml with restart: unless-stopped; run -f compose.yaml -f compose.prod.yaml config {compose.prod.yaml + restart; so sánh config với hai file -f}.
Solution {Lời giải}
services:
web: { restart: unless-stopped, volumes: [] }
db: { restart: unless-stopped }docker compose -f compose.yaml -f compose.prod.yaml configKey takeaways {Điểm chính}
.envinterpolates the compose file;env_fileloads container env — use${VAR:-default}and never bake secrets into images {.envnội suy file compose;env_filenạp env container — dùng${VAR:-default}và không nhúng secret vào image}.healthcheck+depends_on: condition: service_healthycloses the “DB is running but not ready” gap from Part 5 {healthcheck+depends_on: condition: service_healthyvá khoảng trống “DB đang chạy nhưng chưa sẵn sàng” từ Phần 5}.profileskeep optional dev services off the defaultuppath {profilesgiữ dịch vụ dev tùy chọn ngoài luồngupmặc định}.compose.override.yamland-flayer environments without duplicating the whole stack {compose.override.yamlvà-fxếp tầng môi trường mà không nhân đôi cả stack}.--scalesuits stateless workers; avoid scaling services that publish the same host port {--scalehợp worker stateless; tránh scale dịch vụ publish cùng cổng host}.
Next up {Tiếp theo}
Part 7 — Optimizing & Securing Images: shrink image size, speed up builds, scan for vulnerabilities, and run containers with least privilege — the production hardening pass {Phần 7 — Tối ưu & bảo mật image: thu nhỏ image, tăng tốc build, quét lỗ hổng và chạy container với quyền tối thiểu — bước cứng hóa production}.