docker ps báo container “Up 3 hours” chỉ có nghĩa là tiến trình chưa chết. Ứng dụng bên trong có thể đang kẹt deadlock, hết connection pool hay trả lỗi 500 cho mọi request, Docker vẫn hồn nhiên báo Up. HEALTHCHECK sinh ra để lấp khoảng mù đó: dạy Docker cách hỏi ứng dụng “còn phục vụ được không” thay vì chỉ nhìn tiến trình còn sống hay không.
HEALTHCHECK hoạt động thế nào?
HEALTHCHECK là một lệnh do bạn định nghĩa, Docker chạy nó định kỳ bên trong container. Quy ước chỉ dựa trên exit code: 0 là khỏe, 1 là lỗi. Trạng thái container sẽ chuyển giữa ba giá trị:
starting: trong khoảng start_period sau khi container khởi động.healthy: lần check gần nhất thành công.unhealthy: thất bại liên tiếp đủ số lần retries.
Ví dụ Dockerfile hoàn chỉnh cho một API Node.js:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=20s \
CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1
CMD ["node", "server.js"]
Và bản tương đương trong docker-compose, thêm cả ràng buộc thứ tự khởi động:
services:
api:
build: .
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:3000/healthz']
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
worker:
build: ./worker
depends_on:
api:
condition: service_healthy # worker chỉ chạy khi api đã healthy
Điểm đáng giá của depends_on kèm condition: service_healthy: nó biến healthcheck thành cơ chế điều phối khởi động, chấm dứt kiểu sleep 10 giây cầu may chờ dịch vụ khác lên.
Interval, timeout, retries, start_period nghĩa là gì?
| Tham số | Mặc định | Ý nghĩa | Gợi ý thực tế |
|---|---|---|---|
interval |
30s | Khoảng cách giữa hai lần check | 30s cho hầu hết dịch vụ; 10-15s nếu cần phát hiện nhanh |
timeout |
30s | Quá thời gian này, lần check bị tính là thất bại | 3-5s; endpoint health mà cần hơn 5s thì tự nó có vấn đề |
retries |
3 | Số lần thất bại liên tiếp trước khi thành unhealthy | 3 là điểm cân bằng tốt giữa nhạy và ổn định |
start_period |
0s | Khoảng miễn trừ sau khởi động: thất bại không bị đếm | Đặt bằng thời gian khởi động chậm nhất từng đo được, cộng thêm dư |
Phép tính nên nhớ: thời gian phát hiện tệ nhất xấp xỉ interval × retries cộng timeout. Với cấu hình 30s × 3 lần cộng 5s, một container chết ngay sau lần check thành công sẽ mất tới khoảng 95 giây trước khi bị đánh dấu unhealthy. Muốn phát hiện nhanh hơn thì giảm interval, nhưng đừng giảm retries xuống 1, vì một cú GC pause hay tải đột biến sẽ tạo unhealthy giả.
Riêng start_period là tham số hay bị bỏ quên nhất. Ứng dụng Spring Boot có thể cần 40-60 giây khởi động: không có start_period, container ăn đủ 3 lần thất bại và bị dán nhãn unhealthy ngay từ lúc chưa kịp mở cổng, orchestrator lại giết đi dựng lại, thành vòng lặp vô tận.
Lỗi thường gặp nhất khi viết HEALTHCHECK là gì?
Endpoint health gọi ra dịch vụ ngoài. Đây là lỗi nghiêm trọng nhất và phổ biến nhất. Endpoint /healthz được viết “cho chắc” bằng cách ping cả database, Redis lẫn API bên thứ ba. Hậu quả: dịch vụ ngoài chậm hay sập thì mọi container gọi nó đồng loạt thành unhealthy, orchestrator restart hàng loạt container vốn không có lỗi gì, sự cố nhỏ ở một chỗ thành sự cố dây chuyền cả hệ thống. Các sự cố lớn từng được mổ xẻ công khai trong ngành không ít lần có mặt đúng kịch bản này. Nguyên tắc: health check cơ bản chỉ trả lời “tiến trình này còn phục vụ được không”, tách riêng khỏi readiness check sâu nếu thật sự cần.
Dùng curl trong image không có curl. Check thất bại 100% với lỗi executable file not found, và container khỏe mạnh bị báo unhealthy vĩnh viễn. Image alpine chỉ có wget (bản BusyBox), image distroless không có cả shell. Giải pháp gọn nhất là check bằng chính runtime sẵn có:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=20s \
CMD node -e "fetch('http://127.0.0.1:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
Quên rằng exit code khác 0 và 1 là sai quy ước. curl không có --fail sẽ trả 0 kể cả khi server trả HTTP 500, nghĩa là check của bạn luôn “thành công”. Luôn dùng curl --fail hoặc tự kiểm mã trạng thái.
Check quá nặng. Health check chạy mãi mãi theo chu kỳ, mỗi 30 giây một truy vấn tổng hợp vào database là tự tay thêm tải vô ích. Check nên nhẹ tới mức bỏ qua được trong thống kê hiệu năng.
Debug container unhealthy thế nào?
Khi container bị dán nhãn unhealthy, đừng đoán, hỏi thẳng Docker:
docker inspect --format '{{json .State.Health}}' api | jq
Kết quả chứa 5 lần check gần nhất kèm exit code và output đầy đủ, thường chỉ ra ngay vấn đề: lỗi thiếu binary, timeout, hay HTTP 500 từ ứng dụng. Muốn thử lệnh check thủ công thì docker exec -it api wget -qO- http://127.0.0.1:3000/healthz và xem output tận mắt.
HEALTHCHECK xong rồi, ai báo cho bạn lúc 2 giờ sáng?
HEALTHCHECK chỉ đổi trạng thái container, tự nó không gửi thông báo đi đâu và cũng không restart giúp bạn. Trên server chạy docker-compose không có orchestrator, container unhealthy sẽ nằm im chờ ai đó chạy docker ps nhìn thấy. Mảnh ghép còn thiếu là một hệ thống giám sát đọc trạng thái đó và phát cảnh báo: agent của AgentWatch theo dõi container Docker qua ba tầng gồm trạng thái tiến trình, kết quả HEALTHCHECK và probe HTTP, rồi báo qua Zalo hoặc email khi container rơi khỏi trạng thái khỏe. Gói Free cho 2 server là đủ để thử với hệ compose của bạn, chi tiết trong bảng giá.
Tóm gọn công thức viết HEALTHCHECK tử tế: check nhẹ, chỉ hỏi chính container, timeout ngắn, retries 3, start_period đủ dài cho lần khởi động chậm nhất, và có người nhận tin khi nó chuyển đỏ.
