Giao diện
Docker Compose — Service Discovery, ENV & Healthchecks
🎓 Instructor Profile
Kỹ sư Raizo (Phó CTO HPN) — Chuyên gia kiến trúc microservices, người đã triển khai hàng chục cụm dịch vụ từ startup đến enterprise. Cùng Giáo sư Tom hướng dẫn bạn "nhạc trưởng hoá" quy trình phát triển với Docker Compose.
Bạn đã biết build image, viết Dockerfile chuẩn. Nhưng thực tế, không ứng dụng nào sống một mình. API cần Database, Database cần Cache, Worker cần Message Queue. Bài học này dạy bạn cách kết nối tất cả lại — chỉ với một file YAML duy nhất.
🎯 Mục tiêu
Sau bài học này, bạn sẽ:
- Hiểu tại sao cần Docker Compose và nó giải quyết vấn đề gì
- Viết được
compose.ymlhoàn chỉnh với nhiều services - Nắm cơ chế Service Discovery — các container tìm nhau bằng tên, không bằng IP
- Quản lý Environment Variables an toàn (không leak secrets)
- Cấu hình depends_on + healthcheck để loại bỏ race condition khi khởi động
- Thành thạo bộ lệnh
docker compose up/down/logs/exec
🧠 Phần 1: Concept — Tại sao cần Compose?
1.1 Nỗi đau thực tế: Nhiều container = Nhiều vấn đề
Hãy hình dung bạn đang xây dựng TaskFlow — một SaaS quản lý dự án. Stack công nghệ gồm:
| Service | Vai trò | Image |
|---|---|---|
| API | REST server xử lý business logic | Custom (build từ Dockerfile) |
| PostgreSQL | Database chính lưu trữ dữ liệu | postgres:16-alpine |
| Redis | Cache session + job queue | redis:7-alpine |
| Worker | Xử lý tác vụ nền (gửi email, export) | Custom (cùng codebase với API) |
Không có Compose, bạn phải mở 4 tab Terminal và gõ từng lệnh:
bash
# Tab 1: Tạo network
docker network create taskflow-net
# Tab 2: Chạy PostgreSQL
docker run -d --name db --network taskflow-net \
-e POSTGRES_USER=user -e POSTGRES_PASSWORD=pass \
-e POSTGRES_DB=myapp \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
# Tab 3: Chạy Redis
docker run -d --name redis --network taskflow-net \
redis:7-alpine
# Tab 4: Chạy API (phải đợi DB sẵn sàng)
docker run -d --name api --network taskflow-net \
-p 3000:3000 \
-e DATABASE_URL=postgres://user:pass@db:5432/myapp \
-e REDIS_URL=redis://redis:6379 \
my-api:latest
# Tab 5: Chạy Worker
docker run -d --name worker --network taskflow-net \
-e DATABASE_URL=postgres://user:pass@db:5432/myapp \
-e REDIS_URL=redis://redis:6379 \
my-api:latest npm run worker5 lệnh dài dằng dặc. Sai một tham số là hệ thống "gãy". Quên tạo network? Container không tìm thấy nhau. Quên volume? Mất dữ liệu.
1.2 Compose = Khai báo hạ tầng bằng code (IaC)
Docker Compose giải quyết mọi thứ trên bằng một file YAML duy nhất và một lệnh duy nhất:
bash
docker compose up -d"Define and run multi-container Docker applications." — Tài liệu chính thức Docker
Compose là gì trong bức tranh lớn?
| Công cụ | Phạm vi | Dùng khi nào |
|---|---|---|
| Dockerfile | Build một image | Đóng gói ứng dụng |
| Docker Compose | Orchestrate nhiều containers trên một máy | Development, testing, CI/CD |
| Kubernetes | Orchestrate nhiều containers trên nhiều máy | Production at scale |
⚠️ Compose KHÔNG phải cho production orchestration
Compose tuyệt vời cho local development và testing. Nhưng nếu bạn cần auto-scaling, self-healing, rolling updates trên cluster — đó là việc của Kubernetes. Đừng nhầm lẫn phạm vi sử dụng.
1.3 Tư duy Architect: Tại sao SaaS Startup cần Compose?
Trong thực tế, team phát triển TaskFlow sử dụng Compose để:
- Onboard developer mới trong 5 phút thay vì 2 ngày setup môi trường
- Đảm bảo consistency — mọi người chạy cùng một stack, cùng phiên bản
- Mirror production topology — local dev giống production (API + DB + Redis + Worker)
- Chạy integration tests trong CI/CD pipeline trước khi merge code
Đó là lý do Compose là công cụ bắt buộc trong workflow của mọi team chuyên nghiệp.
📐 Phần 2: Syntax — Giải phẫu compose.yml
2.1 File mẫu thực tế: TaskFlow Stack
Đây là file compose.yml hoàn chỉnh cho hệ thống TaskFlow — đọc kỹ từng dòng comment:
yaml
# compose.yml — TaskFlow Development Stack
# Compose V2: KHÔNG cần "version:" nữa (deprecated từ Compose 2.x)
services:
# ========================================
# Service 1: API Server (Node.js)
# ========================================
api:
build:
context: . # Build context = thư mục hiện tại
dockerfile: Dockerfile # Dùng Dockerfile đã viết từ bài trước
ports:
- "3000:3000" # Expose port ra host để truy cập từ browser
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy # ⏳ Đợi DB THỰC SỰ sẵn sàng
redis:
condition: service_started # Redis khởi động nhanh, không cần healthcheck
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s # Kiểm tra mỗi 10 giây
timeout: 5s # Timeout mỗi lần kiểm tra
retries: 3 # Thử 3 lần trước khi báo unhealthy
start_period: 15s # Grace period cho app khởi động
restart: unless-stopped
# ========================================
# Service 2: PostgreSQL Database
# ========================================
db:
image: postgres:16-alpine # Dùng image có sẵn, không cần build
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data # 📦 Persist data qua restart
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s # DB cần kiểm tra thường xuyên hơn
timeout: 3s
retries: 5 # Cho phép thử nhiều lần (DB khởi động chậm)
restart: unless-stopped
# ========================================
# Service 3: Redis Cache
# ========================================
redis:
image: redis:7-alpine
ports:
- "6379:6379" # Expose cho debug tool (RedisInsight)
restart: unless-stopped
# ========================================
# Khai báo Named Volumes
# ========================================
volumes:
pgdata: # Docker quản lý volume này tự động💡 Compose V2 — Không cần version: nữa
Từ Docker Compose V2, trường version: "3.8" đã bị deprecated. Compose tự phát hiện schema phù hợp. PENALGO dạy chuẩn V2:
- V1 (Cũ):
docker-compose up(có gạch nối) — Đã ngừng phát triển - V2 (Mới):
docker compose up(dấu cách) — Dùng chuẩn này
2.2 Bảng tham chiếu nhanh: Các keyword quan trọng
| Keyword | Mục đích | Ví dụ |
|---|---|---|
services: | Định nghĩa danh sách dịch vụ | services: api: ... |
build: | Build image từ Dockerfile | build: { context: ., dockerfile: Dockerfile } |
image: | Dùng image có sẵn từ registry | image: postgres:16-alpine |
ports: | Map port Host:Container | ports: ["3000:3000"] |
environment: | Đặt biến môi trường | NODE_ENV=development |
volumes: | Mount data | pgdata:/var/lib/postgresql/data |
depends_on: | Thứ tự khởi động + điều kiện | condition: service_healthy |
healthcheck: | Kiểm tra sức khoẻ service | test: ["CMD", "curl", ...] |
restart: | Chính sách restart | unless-stopped, always, on-failure |
🌐 Phần 3: Service Discovery — Các container tìm nhau như thế nào?
3.1 Vấn đề: IP thay đổi liên tục
Mỗi khi bạn chạy docker compose up, container nhận IP ngẫu nhiên từ Docker. Lần này db là 172.18.0.2, lần sau là 172.18.0.4. Nếu API hardcode IP để kết nối DB — chắc chắn gãy.
3.2 Giải pháp: DNS tự động bằng Service Name
Khi chạy docker compose up, Compose tự động:
- Tạo một Docker network riêng cho project (tên:
<tên-thư-mục>_default) - Gắn tất cả services vào network đó
- Tạo DNS entry cho mỗi service: tên service = hostname
Kết quả: API kết nối DB bằng hostname db, không bằng IP.
Connection String trong API:
postgres://user:pass@db:5432/myapp
^^
Tên service, KHÔNG phải IP!3.3 Sơ đồ Network Topology
┌─────────────────────────────────────────────────────────┐
│ Docker Network: taskflow_default │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ api │ │ db │ │ redis │ │
│ │ :3000 │────▶│ :5432 │ │ :6379 │ │
│ │ │ │ │ │ │ │
│ │ │─────────────────────▶│ │ │
│ └────┬─────┘ └──────────┘ └────┬─────┘ │
│ │ │ │
│ │ DNS: "db" → 172.18.0.2 │ │
│ │ DNS: "redis" → 172.18.0.3 │ │
│ │ │ │
└────────┼──────────────────────────────────┼─────────────┘
│ │
ports: 3000:3000 ports: 6379:6379
│ │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ HOST MACHINE │
│ localhost:3000 localhost:6379 │
└─────────────────────────────────────────────────────────┘Quy tắc quan trọng:
| Giao tiếp | Cần ports: không? | Hostname |
|---|---|---|
| Service → Service (trong cùng network) | ❌ KHÔNG | Dùng tên service: db:5432 |
| Host → Container (từ browser/terminal) | ✅ CÓ | localhost:3000 |
💡 Quy tắc vàng
ports: chỉ cần khi bạn muốn truy cập service từ bên ngoài Docker network (browser, Postman, debug tool). Service-to-service communication KHÔNG cần ports:.
3.4 Chứng minh Service Discovery hoạt động
bash
# Từ container 'api', ping container 'db'
docker compose exec api ping db -c 3
# Kết quả:
# PING db (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms
# 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.095 ms
# Từ container 'api', resolve DNS của 'redis'
docker compose exec api nslookup redis
# Kết quả:
# Name: redis
# Address: 172.18.0.3Service Discovery = tự động, không cần cấu hình gì thêm. Đó là sức mạnh của Compose.
🔐 Phần 4: Environment Handling — Quản lý biến môi trường
4.1 Ba cách đặt biến môi trường
Cách 1: Trực tiếp trong compose.yml (Hardcoded)
yaml
services:
api:
environment:
- NODE_ENV=development # Dạng list
POSTGRES_USER: user # Dạng mappingKhi nào dùng: Chỉ cho giá trị không nhạy cảm (NODE_ENV, LOG_LEVEL, PORT).
Cách 2: Dùng file .env riêng biệt
Tạo file .env cùng thư mục với compose.yml:
ini
# .env — KHÔNG BAO GIỜ commit file này lên Git!
DB_USER=admin
DB_PASSWORD=SuperSecret123!@#
DB_NAME=taskflow
REDIS_PASSWORD=RedisSecret456
API_PORT=3000Tham chiếu trong compose.yml bằng cú pháp ${VARIABLE}:
yaml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
api:
ports:
- "${API_PORT}:3000"
environment:
- DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}Cách 3: Dùng env_file: trỏ đến file riêng
yaml
services:
api:
env_file:
- ./config/api.env # Load tất cả biến từ file này
environment:
- NODE_ENV=development # Override hoặc bổ sungFile config/api.env:
ini
DATABASE_URL=postgres://admin:secret@db:5432/taskflow
REDIS_URL=redis://redis:6379
JWT_SECRET=my-super-secret-key4.2 Variable Substitution nâng cao
Compose hỗ trợ default values — cực hữu ích để tránh crash khi thiếu biến:
yaml
services:
api:
environment:
# Nếu API_PORT chưa được set → dùng giá trị mặc định 3000
- PORT=${API_PORT:-3000}
# Nếu NODE_ENV chưa được set → dùng "development"
- NODE_ENV=${NODE_ENV:-development}
ports:
# Port mapping cũng dùng được variable substitution
- "${API_PORT:-3000}:3000"| Cú pháp | Ý nghĩa |
|---|---|
${VAR} | Lấy giá trị của VAR. Lỗi nếu không tồn tại |
${VAR:-default} | Lấy VAR, nếu không có → dùng default |
${VAR:?error message} | Lấy VAR, nếu không có → dừng và báo lỗi |
4.3 Thứ tự ưu tiên (Precedence)
Khi cùng một biến được đặt ở nhiều nơi, Compose áp dụng thứ tự sau (cao → thấp):
- Shell environment (export trên terminal) — ưu tiên cao nhất
.envfile tại thư mục chứacompose.ymlenv_file:được khai báo trongcompose.ymlenvironment:trực tiếp trongcompose.yml- Dockerfile
ENV— ưu tiên thấp nhất
⚠️ KHÔNG BAO GIỜ commit .env lên Git
File .env chứa mật khẩu, API keys, secrets. Commit lên Git = lộ thông tin toàn bộ hệ thống.
bash
# Thêm vào .gitignore NGAY LẬP TỨC
echo ".env" >> .gitignore
echo "*.env" >> .gitignore
# Tạo file mẫu để team biết cần những biến gì
cp .env .env.example
# Xóa giá trị nhạy cảm trong .env.example, chỉ giữ keyTạo file .env.example (commit file này):
ini
# .env.example — Template cho team
DB_USER=
DB_PASSWORD=
DB_NAME=
REDIS_PASSWORD=
API_PORT=3000🚦 Phần 5: depends_on + Healthchecks — Giải quyết Race Condition
5.1 Vấn đề: Container chạy ≠ Service sẵn sàng
Đây là kiến thức Deep Tech mà hầu hết Junior Developer bỏ qua — và trả giá bằng hàng giờ debug.
Kịch bản:
- Compose bật container
db(PostgreSQL) lên trước - PostgreSQL cần 5-15 giây để khởi tạo database
- Compose thấy container
dbđãrunning→ bật containerapilên ngay - API cố kết nối
db:5432→ "Connection Refused" → Crash 💥
Timeline thảm hoạ:
t=0s t=1s t=2s t=12s
│ │ │ │
▼ ▼ ▼ ▼
[db start] [api start] [api crash!] [db ready]
api kết nối "Connection Quá muộn!
db:5432... Refused" API đã chết5.2 depends_on thuần tuý — Chỉ là thứ tự, KHÔNG đợi sẵn sàng
yaml
# ❌ SAI — Lỗi phổ biến nhất
services:
api:
depends_on:
- db # Chỉ đảm bảo container db KHỞI ĐỘNG trước
# KHÔNG đảm bảo PostgreSQL ĐÃ SẴN SÀNG nhận kết nối!5.3 Giải pháp đúng: Healthcheck + condition: service_healthy
yaml
# ✅ ĐÚNG — Đợi service THỰC SỰ sẵn sàng
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"] # Lệnh kiểm tra sức khoẻ
interval: 5s # Kiểm tra mỗi 5 giây
timeout: 3s # Timeout mỗi lần check
retries: 5 # Thử 5 lần trước khi báo unhealthy
start_period: 10s # Grace period ban đầu — không tính failure
api:
depends_on:
db:
condition: service_healthy # 🟢 CHỈ KHỞI ĐỘNG KHI DB HEALTHYTimeline đúng:
t=0s t=5s t=10s t=12s t=13s
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[db start] [check 1] [check 2] [db healthy] [api start]
pg_isready pg_isready pg_isready Kết nối
→ FAIL → FAIL → OK ✅ db:5432 → OK ✅5.4 Healthcheck cho các service phổ biến
| Service | Healthcheck command | Giải thích |
|---|---|---|
| PostgreSQL | pg_isready -U user | Kiểm tra DB sẵn sàng nhận kết nối |
| MySQL | mysqladmin ping -h localhost | Ping MySQL daemon |
| Redis | redis-cli ping | Phản hồi PONG nếu healthy |
| HTTP API | curl -f http://localhost:3000/health | Kiểm tra endpoint /health |
| MongoDB | mongosh --eval "db.runCommand('ping')" | Ping MongoDB |
5.5 Bảng so sánh condition
| Condition | Hành vi | Khi nào dùng |
|---|---|---|
service_started | Đợi container khởi động (mặc định) | Service khởi động nhanh (Redis, Nginx) |
service_healthy | Đợi healthcheck báo healthy | Service cần thời gian init (DB, API phức tạp) |
service_completed_successfully | Đợi container chạy xong và exit 0 | Migration script, seed data |
🔬 Phần 6: Lab — Xây dựng TaskFlow Stack hoàn chỉnh
Bước 1: Tạo cấu trúc project
bash
mkdir taskflow && cd taskflow
# Tạo compose.yml (sẽ viết nội dung bên dưới)
touch compose.yml
# Tạo file .env
touch .env
# Tạo .gitignore
echo ".env" > .gitignoreBước 2: Viết file .env
ini
# .env
DB_USER=taskflow_admin
DB_PASSWORD=Str0ngP@ssw0rd!
DB_NAME=taskflow_db
API_PORT=3000Bước 3: Viết compose.yml hoàn chỉnh
yaml
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "${API_PORT:-3000}:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stopped
volumes:
pgdata:Bước 4: Khởi động stack
bash
# Dựng toàn bộ stack và chạy nền (detached)
docker compose up -d
# Output kỳ vọng:
# [+] Running 4/4
# ✔ Network taskflow_default Created
# ✔ Volume taskflow_pgdata Created
# ✔ Container taskflow-db-1 Healthy
# ✔ Container taskflow-redis-1 Started
# ✔ Container taskflow-api-1 StartedBước 5: Kiểm tra trạng thái
bash
# Xem tất cả services và status
docker compose ps
# Output:
# NAME STATUS PORTS
# taskflow-api-1 Up (healthy) 0.0.0.0:3000->3000/tcp
# taskflow-db-1 Up (healthy) 5432/tcp
# taskflow-redis-1 Up 0.0.0.0:6379->6379/tcpBước 6: Debug logs
bash
# Xem log của api service (theo dõi real-time)
docker compose logs -f api
# Xem log của tất cả services (50 dòng gần nhất)
docker compose logs --tail 50
# Xem log của db khi có vấn đề
docker compose logs dbBước 7: Kiểm tra Service Discovery
bash
# Từ container api, ping container db
docker compose exec api ping db -c 3
# → PING db (172.18.0.2): 56 data bytes ...
# Kiểm tra DNS resolution
docker compose exec api nslookup redis
# → Name: redis Address: 172.18.0.3
# Kiểm tra kết nối PostgreSQL từ api
docker compose exec api sh -c \
"apt-get update > /dev/null && apt-get install -y postgresql-client > /dev/null && \
pg_isready -h db -U \$POSTGRES_USER"
# → db:5432 - accepting connectionsBước 8: Dọn dẹp
bash
# Tắt stack, xóa containers và networks (GIỮ LẠI data)
docker compose down
# Tắt stack và XÓA SẠCH cả volumes (MẤT DATA!)
docker compose down -v
# Xóa images đã build
docker compose down --rmi local🐛 Phần 7: Spot-the-Bug — Tìm lỗi trong compose.yml
Bài tập: Service api không kết nối được db
Developer nhận bug report: "API khởi động nhưng trả về 500 — Cannot connect to database". Hãy tìm tất cả lỗi trong file compose.yml sau:
yaml
services:
api:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://admin:secret@database:5432/myapp # 🔍 Lỗi 1?
depends_on:
- db # 🔍 Lỗi 2?
networks:
- frontend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
networks:
- backend
redis:
image: redis:7-alpine
networks:
frontend:
backend:👉 Bấm để xem đáp án
Có 3 lỗi nghiêm trọng:
Lỗi 1: Sai hostname trong DATABASE_URL
DATABASE_URL=postgres://admin:secret@database:5432/myapp
^^^^^^^^
Sai! Service tên là "db" chứ không phải "database"
Phải sửa thành: @db:5432Docker DNS chỉ resolve đúng tên service trong compose.yml. database không match service nào → DNS lookup fail → Connection Refused.
Lỗi 2: depends_on không có condition: service_healthy
yaml
depends_on:
- db # Chỉ đợi container start, KHÔNG đợi PostgreSQL sẵn sàngThiếu healthcheck cho db và thiếu condition: service_healthy. Kết quả: API khởi động trước khi DB ready → crash.
Lỗi 3: api và db nằm trên network khác nhau!
yaml
api: networks: [frontend] # Chỉ thuộc "frontend"
db: networks: [backend] # Chỉ thuộc "backend"Hai service nằm trên hai networks tách biệt → không thể giao tiếp. api phải thuộc cả frontend lẫn backend, hoặc cả hai phải dùng chung network.
File đã sửa:
yaml
services:
api:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://admin:secret@db:5432/myapp # ✅ Đúng tên service
depends_on:
db:
condition: service_healthy # ✅ Đợi DB healthy
networks:
- frontend
- backend # ✅ Thuộc cả 2 networks
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
healthcheck: # ✅ Thêm healthcheck
test: ["CMD-SHELL", "pg_isready -U admin"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
networks:
frontend:
backend:🧩 Phần 8: Parsons Problem — Sắp xếp lại compose.yml
Các khối YAML dưới đây đã bị xáo trộn. Hãy sắp xếp lại thành file compose.yml hợp lệ để: web đợi db healthy trước khi khởi động.
yaml
# Khối A
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 3
# Khối B
services:
# Khối C
web:
image: nginx:alpine
ports:
- "80:80"
# Khối D
depends_on:
db:
condition: service_healthy
# Khối E
db:
image: postgres:16-alpine👉 Bấm để xem đáp án
Thứ tự đúng: B → E → A → C → D
yaml
services: # B
db: # E
image: postgres:16-alpine
healthcheck: # A
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 3
web: # C
image: nginx:alpine
ports:
- "80:80"
depends_on: # D
db:
condition: service_healthyLogic: db phải được khai báo với healthcheck trước khi web có thể depends_on nó với condition: service_healthy.
🎯 Phần 9: Scenario — Xử lý tình huống thực tế
Tình huống: Team mới join startup, cần setup môi trường dev
Bạn vừa join team phát triển TaskFlow. Tech Lead giao việc:
"Setup local dev environment: API (Node.js) + PostgreSQL + Redis. Yêu cầu: một lệnh duy nhất để dựng toàn bộ stack. Phải có healthcheck cho DB. Secrets không được hardcode."
Thử thách: Viết file compose.yml và .env hoàn chỉnh đáp ứng yêu cầu trên. Sau đó chạy:
bash
# Kiểm tra cú pháp (dry run)
docker compose config
# Dựng stack
docker compose up -d
# Verify: tất cả services healthy
docker compose ps
# Verify: service discovery hoạt động
docker compose exec api ping db -c 1Tiêu chí đánh giá:
- [ ] Tất cả secrets trong
.env, không hardcode trongcompose.yml - [ ]
dbcóhealthcheck+apidùngcondition: service_healthy - [ ] Variable substitution
${VAR:-default}cho giá trị không nhạy cảm - [ ]
.gitignorechứa.env
⚠️ Pitfalls — Các bẫy thường gặp
⚠️ Cạm bẫy
Vấn đề: Dùng depends_on: [db] mà nghĩ rằng DB đã ready khi app khởi động.
Hậu quả: depends_on chỉ đảm bảo container được tạo trước. PostgreSQL cần 5-15 giây để initialize. App kết nối ngay → "Connection Refused" → crash loop. Trong CI/CD, test flaky vì race condition.
Giải pháp: Luôn kết hợp healthcheck + condition: service_healthy. Đây là bắt buộc cho mọi project.
yaml
# ❌ Race condition
depends_on:
- db
# ✅ An toàn
depends_on:
db:
condition: service_healthy⚠️ Cạm bẫy
Vấn đề: Service tên db nhưng connection string dùng @database:5432 hoặc @localhost:5432.
Hậu quả: DNS lookup fail → Connection timeout → App crash.
Giải pháp: Hostname trong connection string phải trùng với tên service trong compose.yml. localhost chỉ trỏ đến chính container đó, không phải container khác.
yaml
# Service tên "db" → hostname phải là "db"
DATABASE_URL=postgres://user:pass@db:5432/myapp
# ^^
# Phải match tên service!⚠️ Cạm bẫy
Vấn đề: Khai báo custom networks nhưng quên gắn service vào đúng network.
Hậu quả: Services nằm trên networks khác nhau → hoàn toàn cô lập → không thể giao tiếp.
Giải pháp: Nếu không cần phân tách network, đừng khai báo custom networks — Compose tự tạo default network cho tất cả services. Nếu cần phân tách, đảm bảo services cần giao tiếp nằm trên cùng ít nhất một network.
💡 Tips & Performance
🚀 docker compose watch — Live reload cho development
Từ Compose 2.22+, bạn có thể dùng docker compose watch để tự động sync code thay đổi vào container — không cần rebuild:
yaml
services:
api:
build: .
develop:
watch:
- action: sync # Sync file thay đổi vào container
path: ./src
target: /app/src
- action: rebuild # Rebuild image khi package.json thay đổi
path: ./package.jsonbash
# Thay thế bind mount + nodemon
docker compose watchLợi ích: Nhanh hơn bind mount trên macOS/Windows (tránh vấn đề filesystem performance), và rõ ràng hơn về intent.
💡 Kiểm tra cú pháp trước khi chạy
bash
# Validate compose.yml — phát hiện lỗi YAML trước khi dựng stack
docker compose config
# Chỉ xem services nào sẽ được tạo
docker compose config --services
# Xem file compose đã được resolve (sau variable substitution)
docker compose config --format json💡 Tối ưu thời gian build với cache
bash
# Build song song tất cả services (nhanh hơn nhiều)
docker compose build --parallel
# Build chỉ service cần thiết
docker compose build api
# Force rebuild (bỏ qua cache)
docker compose build --no-cache api🛑 Anti-patterns — Những điều KHÔNG BAO GIỜ làm
🛑 Anti-pattern 1: Hardcode password trong compose.yml và commit lên Git
yaml
# ❌ TUYỆT ĐỐI KHÔNG LÀM ĐIỀU NÀY
services:
db:
environment:
POSTGRES_PASSWORD: MySecretPassword123!Hậu quả: Mọi người có quyền truy cập repo đều thấy mật khẩu. Nếu repo public — cả internet thấy. Bot tự động quét GitHub repositories tìm credentials — bạn sẽ bị hack trong vòng vài phút.
yaml
# ✅ Dùng biến môi trường từ .env
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}🛑 Anti-pattern 2: Sử dụng links: (Deprecated)
yaml
# ❌ DEPRECATED — Không dùng nữa!
services:
api:
links:
- db
- redislinks: là tính năng từ Compose V1, đã bị deprecated. Docker network tự động cung cấp DNS resolution — links: hoàn toàn không cần thiết và chỉ gây nhầm lẫn.
yaml
# ✅ Không cần khai báo gì — DNS tự động hoạt động
services:
api:
depends_on:
db:
condition: service_healthy
# Trong code: kết nối db:5432 — tự động resolve🛑 Anti-pattern 3: Expose port DB/Cache ra ngoài host
yaml
# ❌ NGUY HIỂM — Database bị expose ra mạng
services:
db:
ports:
- "5432:5432" # Ai trên cùng mạng đều kết nối được!Chỉ expose port cho services cần truy cập từ bên ngoài Docker (API, frontend). DB và Cache không bao giờ cần expose port trừ khi debug.
yaml
# ✅ DB không expose port — chỉ có service cùng network kết nối được
services:
db:
image: postgres:16-alpine
# KHÔNG có "ports:" — an toàn⚙️ Phần 10: Under the Hood — Compose hoạt động như thế nào?
10.1 Docker Network Bridge Mode
Khi bạn chạy docker compose up, đây là những gì xảy ra bên dưới:
1. Compose đọc compose.yml
2. Tạo Docker network kiểu "bridge" (mặc định):
→ docker network create taskflow_default --driver bridge
3. Với mỗi service, tạo container và gắn vào network:
→ docker create --name taskflow-db-1 --network taskflow_default postgres:16-alpine
→ docker create --name taskflow-redis-1 --network taskflow_default redis:7-alpine
→ docker create --name taskflow-api-1 --network taskflow_default my-api:latest
4. Docker daemon tạo DNS alias cho mỗi container:
→ "db" → 172.18.0.2
→ "redis" → 172.18.0.3
→ "api" → 172.18.0.4
5. Khởi động containers theo thứ tự depends_on:
→ Start db → đợi healthcheck → Start redis → Start api10.2 DNS Resolution bên trong
Mỗi container nhận một embedded DNS resolver tại 127.0.0.11:53. Khi api gọi db:5432:
api container Docker DNS (127.0.0.11) db container
│ │ │
│ DNS query: "db" │ │
│─────────────────────────────▶│ │
│ │ Resolve: db → 172.18.0.2 │
│ DNS response: 172.18.0.2 │ │
│◀─────────────────────────────│ │
│ │
│ TCP connect: 172.18.0.2:5432 │
│──────────────────────────────────────────────────────────▶│
│ Connection established │
│◀──────────────────────────────────────────────────────────│10.3 Port Mapping vs Service-to-Service
┌─────────────────────────────────────────────────────┐
│ Docker Bridge Network │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ api │──────▶│ db │ │ redis │ │
│ │ :3000 │ TCP │ :5432 │ │ :6379 │ │
│ │ │──────────────────────▶│ │ │
│ └────┬────┘ └─────────┘ └────┬────┘ │
│ │ (không cần ports:) │ │
│ │ │ │
└───────┼───────────────────────────────────┼─────────┘
│ │
ports: 3000:3000 ports: 6379:6379
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ HOST MACHINE │
│ browser → localhost:3000 │
│ RedisInsight → localhost:6379 │
└─────────────────────────────────────────────┘Tóm tắt:
- Port mapping (
ports:) = Cầu nối giữa host machine và Docker network. Chỉ cần khi truy cập từ browser/tool bên ngoài - Service-to-service = Giao tiếp trực tiếp qua Docker network. Dùng tên service làm hostname. Không cần
ports:
10.4 Tại sao localhost không hoạt động giữa containers?
yaml
# ❌ HIỂU SAI PHỔ BIẾN
environment:
- DATABASE_URL=postgres://user:pass@localhost:5432/myappMỗi container là một Linux namespace riêng biệt — có network stack riêng. localhost trong container api trỏ đến chính container api, không phải container db. Đó là lý do phải dùng tên service (db) thay vì localhost.
📝 Quiz: Kiểm tra kiến thức
🧠 Quiz
Câu 1: Trong Docker Compose, service api muốn kết nối PostgreSQL (service tên db). Connection string nào đúng?
- [ ] A.
postgres://user:pass@localhost:5432/myapp - [ ] B.
postgres://user:pass@172.18.0.2:5432/myapp - [x] C.
postgres://user:pass@db:5432/myapp - [ ] D.
postgres://user:pass@postgres:5432/myapp
💡 Giải thích: Docker Compose tạo DNS tự động — hostname = tên service trong
compose.yml. Service têndb→ hostname làdb. Không dùnglocalhost(trỏ đến chính container), không dùng IP (thay đổi mỗi lần restart), không dùng tên image (postgres).
🧠 Quiz
Câu 2: Sự khác biệt giữa depends_on: [db] và depends_on: { db: { condition: service_healthy } }?
- [ ] A. Không có sự khác biệt — cả hai đều đợi DB sẵn sàng
- [ ] B. Phiên bản dài chỉ hỗ trợ trong Compose V1
- [x] C. Phiên bản ngắn chỉ đợi container start; phiên bản dài đợi healthcheck pass
- [ ] D. Phiên bản dài khiến Compose tự restart DB nếu nó crash
💡 Giải thích:
depends_on: [db]chỉ đảm bảo containerdbđược tạo trước — KHÔNG kiểm tra service bên trong đã sẵn sàng chưa.condition: service_healthybắt buộc Docker đợi healthcheck pass mới khởi động service phụ thuộc. Đây là cách duy nhất tránh race condition.
🧠 Quiz
Câu 3: Khi nào service KHÔNG cần khai báo ports: trong compose.yml?
- [ ] A. Khi service đó không lắng nghe trên port nào
- [x] B. Khi service chỉ được truy cập bởi các services khác trong cùng Docker network
- [ ] C. Khi service dùng UDP thay vì TCP
- [ ] D. Khi service dùng image từ Docker Hub
💡 Giải thích:
ports:chỉ map port từ container ra host machine. Service-to-service communication trong cùng Docker network không cầnports:— chúng giao tiếp trực tiếp qua internal network. VD:dbkhông cầnports: ["5432:5432"]vì chỉapicần kết nối, vàapidùngdb:5432thông qua internal DNS.
🧠 Quiz
Câu 4: Tại sao nên tạo file .env.example và commit lên Git?
- [ ] A. Để Docker Compose tự động sử dụng nếu không tìm thấy
.env - [ ] B. Để CI/CD pipeline đọc giá trị mặc định
- [x] C. Để team biết cần đặt những biến môi trường nào mà không lộ giá trị thực
- [ ] D. Để Compose validate kiểu dữ liệu của biến
💡 Giải thích:
.envchứa secrets thật → KHÔNG commit..env.examplechứa danh sách keys (không có giá trị nhạy cảm) → commit để team mới biết cần tạo những biến gì. Đây là convention phổ biến trong mọi dự án chuyên nghiệp.
✅ Compose Development Checklist
✅ Checklist triển khai
Checklist trước khi push code có compose.yml:
- [ ]
.envđã thêm vào.gitignore - [ ] Tất cả secrets dùng variable substitution
${VAR}— không hardcode - [ ] Có file
.env.examplevới danh sách keys (không có giá trị nhạy cảm) - [ ] Mỗi database service có
healthcheckphù hợp - [ ]
depends_ondùngcondition: service_healthycho services cần thời gian init - [ ] Chỉ expose
ports:cho services cần truy cập từ host - [ ] Named volumes cho persistent data (DB, uploads)
- [ ]
restart: unless-stoppedcho tất cả services - [ ]
docker compose configchạy thành công (YAML hợp lệ) - [ ]
docker compose up -d && docker compose ps— tất cả services healthy
🎯 Tổng kết & Bước tiếp theo
Docker Compose biến quy trình phát triển từ "mở 5 terminal gõ 5 lệnh dài" thành "một file YAML + một lệnh docker compose up". Ba trụ cột cần nhớ:
- Service Discovery — Container gọi nhau bằng tên service, DNS tự động
- Environment — Secrets trong
.env, variable substitution${VAR:-default} - Healthchecks —
depends_on+condition: service_healthy= không còn race condition
Bạn đã hoàn thành Phase 1 Docker track! 🎉 Tiếp theo, hãy thực hành tại Practice Lab hoặc tiến vào Module 3: Compose nâng cao để học thêm về multi-network architecture, secrets management, và deploy configuration.