Bạn vừa deploy website bằng Docker Compose. Gõ lệnh docker compose ps, bạn thấy toàn bộ container đều hiển thị trạng thái Up (healthy). Nhưng khi mở trình duyệt lên, website lại trả về lỗi 502 Bad Gateway, hoặc form gửi dữ liệu báo lỗi kết nối database.
Tình trạng "màn hình báo xanh nhưng thực tế đang chết" này rất phổ biến khi healthcheck được cấu hình qua loa:
- Hoặc bạn chỉ kiểm tra xem tiến trình chính có còn chạy hay không (
pidof,ps). Tiến trình chưa crash không có nghĩa là nó đang nhận request; app có thể đang bị treo (deadlock) hoặc kẹt vòng lặp vô tận. - Hoặc bạn trỏ healthcheck vào một endpoint dummy trả về mã
200 OKtĩnh. Endpoint này trả 200 ngay cả khi app chưa kết nối được MySQL hay Redis.
Mục tiêu duy nhất của một healthcheck tử tế trong Docker Compose là trả lời được câu hỏi: Ứng dụng bên trong container này đã thực sự sẵn sàng nhận việc hay chưa?
4 thông số bắt buộc phải hiểu trong block healthcheck
Một cấu hình healthcheck cơ bản trong compose.yaml thường có dạng:
services:
web:
image: my-app:1.2.0
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30sĐể tránh việc container bị Docker đánh giá sai sức khỏe, bạn cần đặt 4 thông số này dựa trên đặc tính thực tế của ứng dụng:
start_period(Thời gian ân hạn khởi động): Đây là thông số hay bị bỏ quên nhất. Một ứng dụng Node.js, Spring Boot hay Python khi vừa bật lên cần từ 10 đến 30 giây để nạp cấu hình, kết nối database và khởi tạo bộ nhớ. Trong khoảng thời gianstart_period, nếu healthcheck có thất bại thì Docker cũng không tính vào số lần lỗi. Nếu không đặt thông số này, container rất dễ bị Docker kết luận làunhealthyngay từ lúc đang boot, dẫn đến việc bị restart liên tục (crash loop) oan uổng.interval(Tần suất kiểm tra): Khoảng thời gian giữa hai lần chạy check. Đừng đặt quá ngắn (như 2s hay 5s) vì việc chạy lệnh lặp lại liên tục sẽ tạo ra tải ảo và ghi ngập log hệ thống. Mức 20s đến 30s là đủ an toàn cho hầu hết website.timeout(Thời gian chờ tối đa): Nếu sau khoảng thời gian này (thường là 3s – 5s) mà lệnh kiểm tra không phản hồi, Docker coi lần đó là thất bại.retries(Số lần thử lại liên tiếp): Số lần thất bại liên tiếp trước khi container chính thức bị đổi trạng thái thànhunhealthy. Đặt từ 3 đến 5 lần để tránh việc mạng nội bộ chập chờn nhất thời làm container bị báo lỗi sai.
Cách viết healthcheck chuẩn cho từng loại dịch vụ
Mỗi loại dịch vụ có cách đo sức khỏe khác nhau. Đừng sao chép một lệnh chung cho mọi container.
1. Dịch vụ Web Application (Node.js, PHP, Python, Go)
Với ứng dụng web, cách đáng tin cậy nhất là tạo một route nội bộ (thường đặt là /healthz hoặc /ping) trong mã nguồn của bạn.
healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:8080/healthz || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s- Lưu ý cờ
-fcủa curl: Cờ-f(hoặc--fail) cực kỳ quan trọng. Nếu không có cờ này, khi server trả về mã lỗi500 Internal Server Error,curlvẫn trả về exit code0(thành công) vì nó vẫn nhận được phản hồi HTTP. Docker sẽ tưởng container khỏe mạnh trong khi web đang sập. - Đừng nhồi nhét kiểm tra API bên ngoài: Route
/healthzchỉ nên kiểm tra nhẹ: ứng dụng đã nạp xong bộ nhớ và kết nối được database nội bộ hay chưa. Tuyệt đối không kiểm tra các dịch vụ bên thứ ba (như cổng thanh toán, dịch vụ gửi SMS hay email) bên trong healthcheck. Nếu cổng thanh toán quốc tế bị chập chờn 5 phút, container web của bạn sẽ bị Docker coi làunhealthyvà khởi động lại oan uổng.
2. Dịch vụ Database (PostgreSQL & MySQL)
Đừng dùng lệnh ping cổng TCP (như nc hay telnet) cho database. Cổng có thể mở nhưng cơ chế xác thực hoặc bộ nhớ của database chưa sẵn sàng nhận truy vấn. Hãy dùng chính công cụ dòng lệnh của cơ sở dữ liệu:
- Với PostgreSQL:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s- Với MySQL / MariaDB:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD} || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s3. Dịch vụ Cache (Redis)
Redis có sẵn công cụ redis-cli cực nhẹ:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 15s
timeout: 3s
retries: 3Lệnh này gửi một lệnh ping đơn giản và nhận lại phản hồi PONG, xác nhận Redis đã sẵn sàng lưu và đọc dữ liệu.
4. Background Worker (Tiến trình xử lý hàng đợi)
Các worker xử lý hàng đợi (như Celery, Laravel Queue, BullMQ) không mở cổng HTTP. Sai lầm phổ biến là cố cài thêm một web server nhỏ chỉ để trả về mã 200 OK. Việc này rất nguy hiểm: web server giả tạo đó vẫn trả 200 trong khi luồng xử lý worker bên trong đã bị treo cứng (deadlock).
Cách thực tế hơn là dùng cơ chế ghi file tín hiệu (heartbeat file). Cứ sau mỗi lượt quét hàng đợi thành công, worker ghi đè một timestamp vào file /tmp/worker-heartbeat. Lệnh healthcheck chỉ cần kiểm tra xem file đó có được cập nhật trong vòng 2 phút gần nhất hay không:
healthcheck:
test: ["CMD-SHELL", "test $(find /tmp/worker-heartbeat -mmin -2 2>/dev/null) || exit 1"]
interval: 30s
timeout: 5s
retries: 3Nếu worker bị treo, file heartbeat sẽ cũ đi, lệnh find không tìm thấy file và Docker sẽ phát hiện ra ngay worker đã ngừng làm việc.
Dùng depends_on: service_healthy để dẹp bỏ lỗi khởi động
Một trong những lỗi kinh điển khi chạy nhiều container là: Web app bật lên trước khi Database sẵn sàng, dẫn đến việc app bị crash ngay từ giây đầu tiên.
Theo mặc định, nếu bạn chỉ khai báo depends_on: [db], Docker Compose chỉ chờ cho container db được tạo xong (process vừa chạy) là đã lập tức kích hoạt container web. Lúc này MySQL hay PostgreSQL vẫn đang trong giai đoạn nạp file dữ liệu và chưa mở cổng kết nối, khiến web app gọi vào là gãy ngay.
Để khắc phục triệt để, hãy kết hợp với điều kiện service_healthy:
services:
web:
image: my-app:1.2.0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10sVới cấu hình này, Docker Compose sẽ kiên nhẫn đợi cho tới khi Postgres thực sự vượt qua bài kiểm tra sức khỏe và chuyển sang màu xanh (healthy) thì mới bắt đầu khởi động container web.
Tuy nhiên, bạn vẫn nên thiết lập cho ứng dụng cơ chế thử lại kết nối (retry với backoff) trong mã nguồn. Điều này giúp ứng dụng không bị chết nếu database bị restart tạm thời trong quá trình hệ thống đang vận hành thực tế.
3 cạm bẫy chí mạng khi viết healthcheck
Trước khi đưa cấu hình lên production, hãy rà soát lại 3 lỗi mà nhiều người rất hay mắc:
- Dùng lệnh kiểm tra mà image không có sẵn: Nếu ứng dụng của bạn đóng gói bằng Alpine hoặc Distroless siêu nhẹ, rất có thể trong image hoàn toàn không có sẵn
curlhaywget. Khi đó lệnh check sẽ trả về lỗicommand not found(exit code 127) và container bị đánh dấuunhealthyoan. Nếu image không có curl, bạn có thể tận dụng chính runtime của app để kiểm tra (ví dụ script Node.js hoặc Python một dòng), hoặc dùngwgetcó sẵn của Alpine. - Healthcheck gây biến đổi dữ liệu (Side-effect): Lệnh healthcheck chạy lặp lại 20–30 giây một lần, suốt ngày đêm. Tuyệt đối không để route healthcheck ghi bản ghi thử vào database, gửi email hay xóa file tạm. Mọi kiểm tra phải là thao tác đọc (read-only) thuần túy.
- Nhầm lẫn giữa Healthcheck nội bộ và Giám sát từ bên ngoài (External Monitoring): Container báo
healthychỉ chứng minh dịch vụ bên trong máy chủ đang sẵn sàng. Nó không đảm bảo khách hàng bên ngoài vào được website nếu chứng chỉ SSL bị hết hạn, tên miền DNS trỏ sai hoặc tường lửa firewall chặn cổng. Bạn vẫn cần kết hợp thêm công cụ kiểm tra uptime từ bên ngoài Internet như hướng dẫn tại bài Uptime monitoring nên kiểm tra HTTP 200 hay nội dung trang?.
Tóm lại
Một cấu hình healthcheck chuẩn xác không làm hệ thống của bạn chạy nhanh hơn, nhưng nó giúp bạn biết chính xác container nào đang gặp sự cố thay vì mò mẫm trong hoang mang. Nó cũng là nền tảng để các lệnh Nâng cấp Docker image an toàn với cờ --wait và quy trình Rollback Docker Compose tự động nhận diện được thời điểm dịch vụ đã phục hồi hoàn toàn.
Nếu bạn đang hoàn thiện hệ thống để đưa lên server, hãy tham khảo thêm Checklist đưa ứng dụng Docker Compose lên production để bao quát toàn bộ các khâu từ volume, secret đến logging.



