Mở docker ps, bạn thấy container status: Up 3 hours. Mọi thứ có vẻ ổn. Nhưng bên trong, app đã deadlock — connection pool cạn, thread worker treo, không serve nổi một request nào. Docker không biết, vì running với Docker chỉ đơn giản là “process PID 1 còn sống”. Nó không biết process đó còn làm được việc hay không.
Đó là lý do HEALTHCHECK tồn tại: chuyển từ monitoring “còn thở không” sang “còn khỏe không”. Bài này giải thích cách định nghĩa health check, các trạng thái (starting/healthy/unhealthy), autoheal sidecar pattern, và sự khác biệt giữa readiness probe với liveness probe trong thế giới Compose.
Tại sao “running” không đủ?
Docker quản lý container qua process model: miễn là process với PID 1 còn chạy, container là running. Nhưng process chạy không đồng nghĩa với app hoạt động đúng. Một vài kịch bản process sống nhưng app chết:
| Tình huống | Process PID 1 | App thực tế |
|---|---|---|
| Deadlock trong app | Still alive | Không serve request nào |
| Connection pool cạn (DB, Redis) | Still alive | Timeout mọi request |
| File descriptor leak, accept() fail | Still alive | Không accept connection mới |
| Logic lỗi — loop infinite trong worker thread | Still alive | Worker không xử lý được job |
| DNS resolution fail không recover | Still alive | API call ra ngoài toàn bộ lỗi |
| Memory pressure — GC pause liên tục | Still alive | Response time 30s+, khách hàng đã timeout |
Trong tất cả trường hợp trên, docker ps vẫn vui vẻ báo Up. Không có healthcheck, orchestrator (kể cả Swarm, Nomad, K8s, hay NGINX upstream) không có cơ chế để biết container đã broken và cần action.
--restart=always) chỉ trigger khi process PID 1 exit — nó không restart container vì app deadlock. Healthcheck là lớp phát hiện; restart policy là lớp hành động. Hai thứ phải đi cùng nhau. Xem thêm Phần 5: Container lifecycle & signal handling để hiểu sâu về restart policy.HEALTHCHECK instruction
Docker cung cấp HEALTHCHECK instruction để định nghĩa command kiểm tra sức khỏe container. Cú pháp trong Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Các tham số
| Tham số | Mặc định | Ý nghĩa |
|---|---|---|
--interval | 30s | Thời gian giữa các lần health check |
--timeout | 30s | Timeout cho mỗi lần chạy health check command |
--retries | 3 | Số lần thất bại liên tiếp trước khi đánh dấu unhealthy |
--start-period | 0s | Thời gian khởi động — health check failure trong khoảng này không tính vào retries |
--start-interval | 5s | Interval trong start-period (Docker Engine 25+) |
Quan trọng nhất là --start-period. Nếu app của bạn cần 15 giây để warm up (load config, connect DB, preload cache), set --start-period=30s để Docker không vội đánh dấu unhealthy trong lúc khởi động.
# Real-world example: Java Spring Boot app
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
Ba trạng thái health
Một container có healthcheck sẽ chuyển qua ba trạng thái:
stateDiagram-v2
[*] --> starting : Container bắt đầu chạy
starting --> healthy : Health check pass<br/>liên tiếp (sau start-period)
starting --> unhealthy : Health check fail<br/>retries+1 lần liên tiếp<br/>(sau start-period)
healthy --> unhealthy : Fail retries+1<br/>lần liên tiếp
unhealthy --> healthy : Pass retries+1<br/>lần liên tiếp
unhealthy --> [*] : Orchestrator restart<br/>hoặc autoheal
- starting: Trạng thái ban đầu, Docker đang chờ kết quả health check đầu tiên hoặc đang trong start-period. Container trong trạng thái này vẫn nhận request — healthcheck chưa có phán quyết gì.
- healthy: Health check command trả về exit code 0. Container đang hoạt động bình thường.
- unhealthy: Health check command fail số lần vượt quá retries. Lúc này orchestrator hoặc autoheal tool nên hành động.
curl -f, flag -f (fail) làm curl trả về exit code 22 nếu HTTP status >= 400, Docker hiểu là unhealthy. Nếu bạn dùng script riêng, nhớ exit 1 khi fail.Health check patterns
Không phải mọi health check đều giống nhau. Tùy loại service mà chọn pattern phù hợp.
Pattern 1: Simple HTTP endpoint
Pattern đơn giản nhất — một endpoint /health trả về 200 nếu app đang chạy:
HEALTHCHECK --interval=15s --timeout=3s --retries=2 \
CMD curl -f http://localhost:3000/health || exit 1
// Express.js health endpoint
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok", uptime: process.uptime() });
});
Pattern này phù hợp cho stateless service, microservice đơn giản. Nhược điểm: không kiểm tra dependency — app có thể trả về 200 trong khi DB đã chết, vì handler không query DB.
Pattern 2: Dependency-aware health check
Health endpoint kiểm tra cả dependency: DB connection, Redis ping, message queue status:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f "http://localhost:8080/health?check=db,redis" || exit 1
// Go health handler với dependency check
func healthHandler(w http.ResponseWriter, r *http.Request) {
checks := r.URL.Query().Get("check")
status := map[string]string{}
if checks == "" || strings.Contains(checks, "db") {
if err := db.Ping(); err != nil {
status["db"] = "unhealthy"
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(status)
return
}
status["db"] = "healthy"
}
if strings.Contains(checks, "redis") {
if err := redisClient.Ping().Err(); err != nil {
status["redis"] = "unhealthy"
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(status)
return
}
status["redis"] = "healthy"
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(status)
}
Lightweight dependency check vs deep check. Khi query parameter check được set, chỉ ping (SELECT 1, PING) — đừng chạy heavy query như COUNT(*) hay full scan. Health check chạy mỗi 30 giây, query nặng sẽ tạo load không cần thiết lên DB. Ping là đủ để biết connection còn sống.
Một lưu ý khác: nếu health check fail vì DB tạm thời chậm, bạn có thực sự muốn restart app container không? Hay chỉ muốn alert? Tách biệt dependency health và app health — dùng monitoring system cho alert, HEALTHCHECK cho liveness của chính app.
Pattern 3: Service-specific health check
Không phải service nào cũng có HTTP endpoint. Với database, message queue, cache — dùng công cụ CLI của chính service đó:
# PostgreSQL
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# MySQL/MariaDB
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
# RabbitMQ
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 30s
timeout: 10s
retries: 3
# MongoDB
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
CMD-SHELL khi cần shell features. CMD form (exec) không resolve biến môi trường ($VAR) hay pipe (|). Nếu health check cần biến env hoặc shell syntax, dùng CMD-SHELL để chạy qua /bin/sh -c. Như ví dụ PostgreSQL ở trên — ${POSTGRES_USER} chỉ được resolve khi chạy qua shell.Autoheal sidecar pattern
Đây là điểm nhiều người bất ngờ: Docker không tự restart container khi health status chuyển sang unhealthy. Docker chỉ cập nhật status trong docker inspect — không có built-in cơ chế restart-khi-unhealthy.
Lý do thiết kế: Docker Engine là low-level container runtime, không phải orchestrator. Việc quyết định “có nên restart container vì unhealthy không” là trách nhiệm của tầng trên: Docker Swarm, Kubernetes, Nomad, hoặc một sidecar container.
Cách 1: Autoheal container (Docker Compose)
Pattern phổ biến: chạy một container autoheal sidecar, mount Docker socket, định kỳ quét unhealthy container và chạy docker restart:
# docker-compose.yml
services:
autoheal:
image: willfarrell/autoheal:latest
tty: true
environment:
- AUTOHEAL_CONTAINER_LABEL=all
- AUTOHEAL_INTERVAL=15 # Quét mỗi 15 giây
- AUTOHEAL_START_PERIOD=300 # 5 phút đầu không restart
- AUTOHEAL_DEFAULT_STOP_TIMEOUT=30
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: always
api:
build: .
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
restart: always
Flow hoạt động: autoheal container mỗi 15 giây gọi Docker API qua socket, liệt kê tất cả container, kiểm tra health status. Nếu thấy unhealthy, nó gọi docker restart <container_id>. Nhờ restart policy always, container bị restart sẽ tự start lại.
Security: mount Docker socket là rủi ro. /var/run/docker.sock cho phép full control Docker daemon — ai có quyền đọc socket này có thể chạy container với --privileged, mount filesystem host, escape container. Autoheal container cần socket để restart container khác, nhưng bạn nên:
- Dùng image tin cậy, update thường xuyên
- Chạy autoheal với user không phải root
- Cân nhắc dùng Docker API proxy thay vì mount socket trực tiếp
- Nếu dùng Swarm/K8s, không cần autoheal — orchestrator có cơ chế built-in
Cách 2: Docker Swarm
Swarm có built-in health checking — khi service replica bị unhealthy, Swarm tự động thay thế bằng container mới:
docker service create \
--name api \
--health-cmd "curl -f http://localhost:8080/health" \
--health-interval 30s \
--health-timeout 5s \
--health-retries 3 \
--health-start-period 60s \
nginx
Trong Swarm mode, không cần autoheal sidecar. Swarm manager liên tục monitor health status và reconcile actual state với desired state — nếu unhealthy, nó kill container cũ và schedule container mới.
Cách 3: Kubernetes
K8s tách biệt hoàn toàn health check khỏi container runtime — dùng probes định nghĩa ở Pod spec level, không phải Dockerfile:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
Kubelet (không phải Docker) chạy probe và restart container khi liveness probe fail. Không cần autoheal, không cần mount socket — orchestrator làm tất cả.
Readiness vs Liveness
Một khái niệm quan trọng mà Docker gộp chung nhưng K8s tách riêng:
| Liveness | Readiness | |
|---|---|---|
| Câu hỏi | “App còn sống không?” | “App đã sẵn sàng nhận request chưa?” |
| Khi fail | Restart container | Ngừng gửi traffic đến pod |
| Khi pass | Container đang chạy | Container có thể serve |
| Ví dụ fail | Deadlock, infinite loop | Chưa load xong config, DB chưa connect được |
| Docker | HEALTHCHECK (gộp cả hai) | HEALTHCHECK (gộp cả hai) |
| K8s | livenessProbe | readinessProbe |
Trong Docker, HEALTHCHECK làm cả hai vai trò — không có distinction. Container unhealthy sẽ bị đánh dấu, và nếu bạn có autoheal hoặc orchestrator, nó sẽ bị restart. Nhưng không có cách nào để nói “container này đang healthy (không chết), nhưng chưa sẵn sàng nhận traffic” — đó là hạn chế của mô hình Docker so với K8s.
Khi nào readiness quan trọng hơn liveness? Tưởng tượng app của bạn connect DB khi start. Nếu DB chưa sẵn sàng, app crash — liveness probe fail, container restart, lại crash, lặp vô hạn (crash loop). Với readiness probe riêng, app vẫn chạy (liveness OK), nhưng readiness fail — load balancer không gửi traffic. Khi DB lên, readiness pass, traffic bắt đầu chảy. Không cần restart.
Trong Docker world, bạn mô phỏng bằng cách set --start-period đủ dài để dependency khởi động, hoặc dùng depends_on với condition (phần tiếp theo).
depends_on với condition trong Compose
Compose có depends_on để kiểm soát thứ tự start. Compose v1 chỉ đảm bảo thứ tự start, không đợi service ready. Compose v2 (Compose Specification) thêm condition:
# docker-compose.yml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
api:
build: .
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
Ba loại condition trong Compose v2:
| Condition | Hành vi |
|---|---|
service_started | Chỉ đợi service đã start (không quan tâm healthy) — tương đương Compose v1 |
service_healthy | Đợi service health status là healthy trước khi start service phụ thuộc |
service_completed_successfully | Đợi service exit với code 0 — dùng cho init job, migration |
depends_on không giải quyết được app-level dependency. Compose chỉ đảm bảo DB container healthy trước khi start app container. Nhưng bên trong app, connection pool khởi tạo lúc startup — nếu DB restart sau đó, app vẫn lỗi. Để handle trường hợp này, app cần retry logic khi connect DB, không dựa hoàn toàn vào Compose.Docker health status trong Compose
docker compose ps hiển thị health status của từng service, cực kỳ hữu ích khi debug:
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
myapp-api-1 myapp-api:latest "docker-entrypoint.s…" api 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:8080->8080/tcp
myapp-db-1 postgres:16-alpine "docker-entrypoint.s…" db 2 minutes ago Up 2 minutes (healthy) 5432/tcp
myapp-redis-1 redis:7-alpine "docker-entrypoint.s…" redis 2 minutes ago Up 2 minutes (healthy) 6379/tcp
Trong CI/CD, bạn có thể dùng docker compose ps với filter để chờ service ready trước khi chạy test:
# CI script: chờ tất cả service healthy
docker compose up -d --wait
# --wait flag (Compose v2.24+): block cho đến khi tất cả service
# có điều kiện depends_on được thỏa mãn và health status là healthy
# Cách thủ công: poll health status
timeout 120 bash -c '
until docker compose ps api | grep -q "(healthy)"; do
echo "Waiting for api to be healthy..."
sleep 5
done
'
echo "All services healthy, ready for tests!"
--wait là game changer cho CI. Thay vì sleep 30 (đoán mò), Compose v2.24+ có docker compose up -d --wait — nó block cho đến khi tất cả service thỏa mãn depends_on condition và health status chuyển healthy. Kết hợp với --wait-timeout 120 để set timeout. Điều này loại bỏ hoàn toàn race condition “test chạy trước khi DB sẵn sàng” trong CI pipeline.Tổng kết
Healthcheck là lớp phát hiện lỗi mà mọi container production đều cần. Những điểm chính:
running!= healthy — process sống không đảm bảo app hoạt động. Deadlock, cạn connection pool, memory pressure đều khiến app broken nhưng process vẫn running.- HEALTHCHECK định nghĩa trong Dockerfile với interval, timeout, retries, và start-period hợp lý. Command chỉ cần exit code — 0 là healthy, 1 là unhealthy.
- Ba trạng thái: starting → healthy ↔ unhealthy. Docker không tự restart khi unhealthy — cần autoheal sidecar hoặc orchestrator.
- Autoheal pattern: container sidecar mount Docker socket, watch unhealthy status, gọi
docker restart. Hoặc dùng Swarm/K8s với built-in health check và restart. - depends_on với condition trong Compose v2 đảm bảo thứ tự start đúng: DB healthy trước khi app start. Kết hợp
--waittrong CI để tránh race condition. - Readiness != Liveness: Docker gộp chung, K8s tách riêng. Nếu app của bạn cần phân biệt “chưa sẵn sàng” với “đã chết”, K8s probes là giải pháp.
Câu hỏi hay gặp
Q: Docker có tự restart container khi unhealthy không?
Không. Docker chỉ cập nhật health status trong metadata — nó không có cơ chế restart-khi-unhealthy. Bạn cần autoheal sidecar (cho Compose standalone) hoặc Swarm/K8s (có built-in). Lý do: Docker Engine là container runtime, không phải orchestrator.
Q: Nên set interval bao nhiêu?
Không có con số ma thuật, nhưng nguyên tắc chung:
- Stateless web app: 15-30 giây, timeout 3-5 giây, retries 2-3
- Database: 10 giây, timeout 5 giây, retries 5 (DB start chậm, cần retry nhiều)
- Batch worker: 60 giây hoặc lâu hơn (không phục vụ request real-time, không cần check thường xuyên)
Đừng set interval quá ngắn (< 5 giây) — health check cũng tiêu tốn tài nguyên. Mỗi lần check là một HTTP request hoặc TCP connection.
Q: Health check có ảnh hưởng performance không?
Có, nhưng không đáng kể nếu thiết kế đúng. Mỗi lần check là một lightweight request — curl đến localhost hoặc một lệnh ping DB. Nếu bạn có 100 container, mỗi cái check mỗi 30 giây, đó là ~3 request/giây — không đáng kể. Vấn đề xảy ra khi health check chạy heavy query (SELECT count(*)) hoặc interval quá ngắn — lúc đó health check tự nó trở thành nguồn load.
Q: Làm sao test health check trong development?
# Chạy trực tiếp health check command trong container
docker exec myapp-api curl -f http://localhost:8080/health
echo $? # 0 = healthy, khác 0 = unhealthy
# Xem health status và lịch sử
docker inspect --format='{{json .State.Health}}' myapp-api | jq
Output của docker inspect cho thấy toàn bộ lịch sử check: thời gian, exit code, output của mỗi lần — cực kỳ hữu ích để debug tại sao container bị đánh dấu unhealthy.
Bài tiếp theo (Multi-container): Phần 9: Docker Compose cơ bản, service definition, networks, volumes, environment, và depends_on.