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ốngProcess PID 1App thực tế
Deadlock trong appStill aliveKhông serve request nào
Connection pool cạn (DB, Redis)Still aliveTimeout mọi request
File descriptor leak, accept() failStill aliveKhông accept connection mới
Logic lỗi — loop infinite trong worker threadStill aliveWorker không xử lý được job
DNS resolution fail không recoverStill aliveAPI call ra ngoài toàn bộ lỗi
Memory pressure — GC pause liên tụcStill aliveResponse 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.

Đừng nhầm restart policy với healthcheck. Restart policy (--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
--interval30sThời gian giữa các lần health check
--timeout30sTimeout cho mỗi lần chạy health check command
--retries3Số lần thất bại liên tiếp trước khi đánh dấu unhealthy
--start-period0sThời gian khởi động — health check failure trong khoảng này không tính vào retries
--start-interval5sInterval 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.
Health check command chỉ cần exit code. Docker không quan tâm output của command — chỉ quan tâm exit code. 0 = healthy, 1 = unhealthy. Với 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
Dùng 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:

  1. Dùng image tin cậy, update thường xuyên
  2. Chạy autoheal với user không phải root
  3. Cân nhắc dùng Docker API proxy thay vì mount socket trực tiếp
  4. 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:

LivenessReadiness
Câu hỏi“App còn sống không?”“App đã sẵn sàng nhận request chưa?”
Khi failRestart containerNgừng gửi traffic đến pod
Khi passContainer đang chạyContainer có thể serve
Ví dụ failDeadlock, infinite loopChưa load xong config, DB chưa connect được
DockerHEALTHCHECK (gộp cả hai)HEALTHCHECK (gộp cả hai)
K8slivenessProbereadinessProbe

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:

ConditionHành vi
service_startedChỉ đợ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:

  1. 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.
  2. 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.
  3. Ba trạng thái: starting → healthy ↔ unhealthy. Docker không tự restart khi unhealthy — cần autoheal sidecar hoặc orchestrator.
  4. 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.
  5. 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 --wait trong CI để tránh race condition.
  6. 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.