Khi deploy ứng dụng lên Docker, nhiều developer chỉ kiểm tra container có đang “chạy” hay không qua lệnh docker ps. Nhưng một container đang ở trạng thái running hoàn toàn không đồng nghĩa với việc ứng dụng bên trong hoạt động bình thường. Container chỉ là một tiến trình (process) được cách ly — tiến trình còn sống chỉ chứng minh PID 1 chưa thoát, chứ không chứng minh database còn nhận kết nối, cache còn phản hồi, hay website còn trả về HTTP 200. Bài viết này sẽ hướng dẫn bạn dùng HEALTHCHECK của Docker để kiểm tra sức khỏe thực sự của container, kết hợp tự động phục hồi và cảnh báo — đúng chuẩn vận hành production.
Vì sao container “running” chưa chắc đã “healthy”
Khi bạn chạy docker ps và thấy container Nginx đang Up 3 hours, bạn không biết được rằng upstream PHP-FPM phía sau đã chết từ lâu, hay file cấu hình vừa bị sửa sai khiến mọi request trả về 502. Tương tự, container Redis có thể vẫn Up nhưng đang bị memory pressure, không phản hồi bất kỳ lệnh nào — cache toàn bộ website khi đó coi như sập, mà hệ thống giám sát của bạn không hề hay biết.
Docker định nghĩa các trạng thái vòng đời của container gồm: created, running, restarting, exited, dead. Từ Docker 1.12, cơ chế HEALTHCHECK bổ sung thêm hai trạng thái phụ: healthy và unhealthy. Khi container có HEALTHCHECK, cột STATUS trong docker ps sẽ hiển thị (healthy) hoặc (unhealthy) — đây mới là tín hiệu phản ánh đúng tình trạng ứng dụng, không phải trạng thái tiến trình.
HEALTHCHECK trong Dockerfile: cú pháp và các tham số
Bạn khai báo HEALTHCHECK ngay trong Dockerfile. Docker sẽ chạy lệnh kiểm tra theo chu kỳ, dựa vào exit code của lệnh để kết luận: 0 nghĩa là healthy, 1 nghĩa là unhealthy, 2 là giá trị dành riêng (reserved). Ví dụ với Nginx:
FROM nginx:1.27-alpine
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD wget -qO- http://127.0.0.1/healthz || exit 1
COPY nginx.conf /etc/nginx/conf.d/default.conf
Ý nghĩa từng tham số:
--interval=30s: chu kỳ giữa hai lần kiểm tra. Không nên quá dày (ví dụ 5s) vì mỗi lần kiểm tra đều tiêu tốn tài nguyên của container.--timeout=5s: thời gian tối đa chờ lệnh kiểm tra trả về. Lệnh chạy lâu hơn timeout sẽ bị tính là lần kiểm tra thất bại.--start-period=40s: thời gian “miễn trừ” lúc khởi động. Ứng dụng cần boot lâu (ví dụ Java, hoặc container đợi database) sẽ không bị đánh giá unhealthy ngay trong khoảng thời gian này, dù lệnh kiểm tra fail. Sau start-period, các lần fail mới được tính vào retries.--retries=3: số lần thất bại liên tiếp để chuyển trạng thái sang unhealthy.
Một chi tiết quan trọng: image nginx:alpine dùng BusyBox nên có sẵn wget, nhưng không có curl. Nếu bạn thích dùng curl -f, phải cài curl vào image bằng RUN apk add --no-cache curl. Luôn kiểm tra lệnh healthcheck chạy được ngay trong container trước khi build, tránh tình trạng mọi lần kiểm tra đều fail vì… thiếu công cụ.
Ví dụ HEALTHCHECK cho các service phổ biến
Redis
Cách kiểm tra Redis đơn giản nhất là gửi lệnh PING — server khỏe mạnh sẽ trả về PONG:
FROM redis:7-alpine
HEALTHCHECK --interval=10s --timeout=3s --retries=5 \
CMD redis-cli ping | grep -q PONG || exit 1
Lệnh redis-cli ping in ra PONG khi Redis còn phản hồi, và trả về exit code khác 0 khi mất kết nối. Pipe qua grep -q PONG để chuyển kết quả thành exit code chuẩn cho Docker. Trường hợp Redis bị memory pressure đến mức không xử lý được lệnh, healthcheck này sẽ phát hiện trong vòng 10 giây — nhanh hơn rất nhiều so với việc chờ cache miss lan rộng khắp website.
MySQL / MariaDB
FROM mariadb:11
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
CMD mysqladmin ping -h 127.0.0.1 -u root -p"$MYSQL_ROOT_PASSWORD" --silent || exit 1
mysqladmin ping trả về exit code 0 và thông báo mysqld is alive khi server còn đáp ứng. Cờ --silent giữ output gọn gàng trong log. Lưu ý biến $MYSQL_ROOT_PASSWORD được Docker truyền vào qua environment — nếu dùng image này trong docker-compose, hãy chắc chắn biến đó được khai báo, nếu không lệnh kiểm tra sẽ fail vì thiếu mật khẩu chứ không phải vì database hỏng.
PHP-FPM
PHP-FPM lắng nghe theo giao thức FastCGI chứ không phải HTTP, nên không thể dùng curl để kiểm tra trực tiếp. Pattern phổ biến là dùng tiện ích cgi-fcgi (gói fcgi trên Alpine):
FROM php:8.3-fpm-alpine
RUN apk add --no-cache fcgi
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
Nếu kết nối FastCGI tới cổng 9000 thành công nghĩa là process FPM còn sống và sẵn sàng nhận request — đây là tín hiệu chính xác hơn nhiều so với việc chỉ kiểm tra process có tồn tại hay không.
Khai báo healthcheck trong docker-compose
Khi chạy nhiều service với docker-compose, bạn nên khai báo healthcheck ngay trong file cấu hình thay vì sửa Dockerfile. Cách này linh hoạt hơn vì cùng một image có thể dùng healthcheck khác nhau tùy môi trường:
services:
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s
app:
build: .
depends_on:
redis:
condition: service_healthy
Điểm mấu chốt là condition: service_healthy. depends_on thông thường chỉ đợi container start, không đợi service bên trong sẵn sàng — đây chính là nguồn gốc của những race condition kinh điển: ứng dụng khởi động trước khi database sẵn sàng nhận kết nối, rồi crash, rồi restart liên tục. Với condition: service_healthy, Docker chỉ khởi động service app sau khi Redis đã vượt qua healthcheck — thứ tự khởi động được đảm bảo một cách thực sự, không phải theo may rủi.
Tự động phục hồi container không khỏe
Healthcheck mới chỉ dừng ở mức “phát hiện”. Muốn hệ thống tự phục hồi, bạn cần kết hợp thêm cơ chế restart. Trước hết là restart policy của Docker:
docker run -d --restart=unless-stopped --name app my-app:latest
docker run -d --restart=on-failure:5 --name worker my-worker:latest
unless-stopped tự khởi động lại container khi Docker daemon restart hoặc process thoát; on-failure:5 giới hạn tối đa 5 lần restart liên tiếp trước khi bỏ cuộc. Tuy nhiên cần hiểu rõ giới hạn: restart policy chỉ kích hoạt khi process chính của container thoát. Container đang chạy nhưng bị đánh giá unhealthy sẽ không được restart policy đụng tới — đây là lỗ hổng khiến nhiều hệ thống “chết đứng mà vẫn Up”.
Để xử lý container unhealthy, phổ biến nhất là dùng image willfarrell/autoheal — một daemon nhỏ lắng nghe sự kiện từ Docker daemon và tự restart container nào rơi vào trạng thái unhealthy:
services:
autoheal:
image: willfarrell/autoheal
restart: always
environment:
- AUTOHEAL_CONTAINER_LABEL=all
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Lưu ý bảo mật: container này phải mount /var/run/docker.sock — tức là nó có toàn quyền điều khiển Docker daemon của host. Chỉ chạy autoheal trên host bạn tin cậy, và nên giới hạn phạm vi bằng label thay vì all nếu host chạy nhiều project khác nhau.
Giám sát và cảnh báo qua Telegram
Autoheal xử lý việc phục hồi, nhưng bạn vẫn cần biết hệ thống đã gặp sự cố gì. Kiểm tra trạng thái health bằng tay:
docker inspect --format='{{.State.Health.Status}}' redis
# Ket qua: healthy | unhealthy | starting
Để tự động hóa, viết một script bash đặt trong crontab, chạy mỗi 5 phút và gửi cảnh báo qua Telegram khi phát hiện container không khỏe:
#!/usr/bin/env bash
# monitor-health.sh - canh bao Telegram khi container unhealthy
set -euo pipefail
CONTAINERS=("redis" "app" "nginx")
BOT_TOKEN="${TELEGRAM_BOT_TOKEN:?Chua set BOT_TOKEN}"
CHAT_ID="${TELEGRAM_CHAT_ID:?Chua set CHAT_ID}"
for name in "${CONTAINERS[@]}"; do
status=$(docker inspect --format='{{.State.Health.Status}}' "$name" 2>/dev/null || echo "missing")
if [[ "$status" != "healthy" ]]; then
msg="[ALERT] Container $name dang o trang thai: $status"
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="$msg" >/dev/null
fi
done
# crontab -e
*/5 * * * * /usr/local/bin/monitor-health.sh >> /var/log/health-monitor.log 2>&1
Vài điểm cần giải thích: set -euo pipefail giúp script dừng ngay khi có lệnh lỗi, tránh cảnh báo sai lan truyền; cú pháp ${TELEGRAM_BOT_TOKEN:?Chua set BOT_TOKEN} khiến script thoát kèm thông báo rõ ràng nếu quên khai báo biến môi trường — an toàn hơn nhiều so với việc gửi request tới bot token rỗng. Chạy qua crontab thay vì vòng lặp while true trong container vì crontab tự xử lý việc log, restart và không chiếm thêm một container giám sát.
Case study: WordPress + Redis trên Dokploy
Trong quá trình triển khai stack WordPress + Redis + MariaDB trên Dokploy cho khách hàng tại Code Tốt, chúng tôi từng gặp một sự cố điển hình: container Redis vẫn Up nhưng do cấu hình maxmemory-policy không phù hợp, Redis rơi vào trạng thái memory pressure, không phản hồi lệnh PING. Hệ quả là toàn bộ cache object của WordPress miss, website chậm rõ rệt — trong khi docker ps vẫn hiển thị mọi container bình thường. Việc tìm ra nguyên nhân tốn gần một buổi vì không có tín hiệu nào từ lớp hạ tầng.
Sau sự cố đó, chúng tôi áp dụng đúng bộ ba giải pháp trong bài viết này: healthcheck redis-cli ping cho container Redis, autoheal để tự restart khi unhealthy, và script cảnh báo Telegram cho đội vận hành. Kể từ đó, Redis hỏng được phát hiện trong vòng 10 giây và tự phục hồi mà không cần ai can thiệp. Theo kinh nghiệm triển khai của Khôi Pro, bộ pattern này giúp giảm đáng kể thời gian phản hồi sự cố cho các dự án WordPress và Laravel — chi phí đầu tư chỉ là vài dòng cấu hình.
Kết luận
docker ps chỉ cho bạn biết container đang chạy; HEALTHCHECK mới cho bạn biết ứng dụng đang khỏe. Kết hợp healthcheck với restart policy, autoheal và cảnh báo Telegram, bạn có một hệ thống tự phát hiện và tự phục hồi sự cố — đúng chuẩn vận hành production mà không cần thêm bất kỳ dịch vụ giám sát tốn kém nào. Hãy bắt đầu từ container quan trọng nhất (database, cache, web server), sau đó mở rộng dần ra toàn bộ stack.
Bài viết liên quan: