Lúc 2 giờ sáng và cái bug đã được fix từ 6 tiếng trước
Mình nhớ rõ cái đêm đó. Nhóm dev push hotfix lên Docker Hub lúc 8 giờ tối. Mình đang ăn cơm, nghĩ bụng “thôi để sau SSH vào pull image rồi restart container”. Kết quả: 2 giờ sáng khách hàng nhắn tin hỏi sao app vẫn còn bug cũ. Image mới đã có trên registry từ 6 tiếng trước mà container trên server vẫn chạy image cũ vì… chưa ai pull.
Đó là lần đầu mình tìm hiểu về Watchtower. Từ đó về sau, việc cập nhật container trên server cá nhân gần như không cần đụng tay nữa.
Watchtower là gì và tại sao cần nó?
Nói ngắn gọn: Watchtower là một container chạy nền, theo dõi các container đang hoạt động trên Docker host. Khi phát hiện image mới trên registry (Docker Hub, GHCR, hay private registry), nó tự pull image mới, dừng container cũ, rồi khởi động lại với image mới — không cần ai ngồi canh.
Khác với Portainer (UI quản lý) hay Docker Swarm (orchestration phức tạp), Watchtower giải quyết đúng một vấn đề: tự động hóa vòng lặp pull → stop → run mà bất kỳ ai chạy Docker trên VPS cũng đã làm đi làm lại hàng chục lần.
Trên một production cluster chạy 30+ container, mình từng dùng script bash để poll registry thủ công. Sau khi chuyển sang Watchtower, resource usage giảm được khoảng 40%. Lý do: Watchtower chỉ check image digest — nếu digest không đổi thì bỏ qua hoàn toàn, không pull bừa.
Cơ chế hoạt động
- Watchtower check image digest trên registry theo chu kỳ (mặc định 24 giờ)
- Digest thay đổi → pull image mới → recreate container với toàn bộ cấu hình cũ (volumes, ports, env vars)
- Digest giống nhau → bỏ qua, không làm gì cả
Thực hành: Cài đặt và cấu hình Watchtower
Bước 1: Chạy Watchtower cơ bản
Cách nhanh nhất để thử:
docker run -d \
--name watchtower \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower
Lệnh này mount Docker socket để Watchtower có quyền tương tác với Docker daemon trên host. Mặc định nó check mỗi 24 giờ và cập nhật tất cả container đang chạy.
Muốn test ngay không cần chờ? Thêm --run-once — Watchtower check một lần rồi thoát:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower --run-once
Bước 2: Chỉ theo dõi container cụ thể
Đây là phần mình thấy nhiều người bỏ qua. Mặc định Watchtower update tất cả container — kể cả database, monitoring stack, những thứ bạn tuyệt đối không muốn tự động restart giữa ban ngày.
Cách 1 — Whitelist: truyền tên container vào cuối lệnh, Watchtower chỉ theo dõi đúng những cái đó:
docker run -d \
--name watchtower \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower myapp nginx-proxy
Cách 2 — Label-based: linh hoạt hơn, dùng được với Docker Compose. Bật chế độ này bằng biến môi trường:
docker run -d \
--name watchtower \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_LABEL_ENABLE=true \
containrrr/watchtower
Rồi trong docker-compose.yml, gắn label vào service nào muốn auto-update — còn lại thì không gắn:
services:
myapp:
image: myuser/myapp:latest
labels:
- "com.centurylinklabs.watchtower.enable=true"
postgres:
image: postgres:16
# Không có label → Watchtower bỏ qua, DB an toàn
Bước 3: Cấu hình interval và notification
Check mỗi 5 phút và gửi thông báo Telegram khi có update:
docker run -d \
--name watchtower \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_POLL_INTERVAL=300 \
-e WATCHTOWER_NOTIFICATIONS=shoutrrr \
-e WATCHTOWER_NOTIFICATION_URL="telegram://BOT_TOKEN@telegram?channels=CHAT_ID" \
-e WATCHTOWER_CLEANUP=true \
-e WATCHTOWER_LABEL_ENABLE=true \
containrrr/watchtower
Các biến môi trường đáng chú ý:
WATCHTOWER_POLL_INTERVAL: Số giây giữa các lần check (mặc định 86400 = 24h)WATCHTOWER_CLEANUP=true: Xóa image cũ sau khi update — không có cái này, disk sẽ đầy dần theo tuầnWATCHTOWER_INCLUDE_STOPPED=true: Cập nhật cả container đang stoppedWATCHTOWER_NO_RESTART=true: Chỉ pull image mới, không restart (dùng để pre-pull trước giờ thấp tải)
Bước 4: Tích hợp với CI/CD pipeline
Phần này mới thực sự thú vị. Thay vì để Watchtower poll theo interval, bạn có thể trigger update ngay khi CI/CD push xong image — thông qua HTTP API có sẵn của nó.
Bật HTTP API:
docker run -d \
--name watchtower \
--restart unless-stopped \
-p 8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_HTTP_API_UPDATE=true \
-e WATCHTOWER_HTTP_API_TOKEN="your-secret-token-here" \
-e WATCHTOWER_HTTP_API_PERIODIC_POLLS=true \
-e WATCHTOWER_LABEL_ENABLE=true \
-e WATCHTOWER_CLEANUP=true \
containrrr/watchtower
Trong GitHub Actions, thêm một step gọi Watchtower ngay sau khi push image:
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: myuser/myapp:latest
- name: Trigger Watchtower update
run: |
curl -H "Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}" \
https://yourserver.com:8080/v1/update
Push code → CI build → push image → Watchtower nhận signal → server tự update. Không SSH, không cronjob, không thức đêm.
Lưu ý bảo mật
Mount /var/run/docker.sock là root-equivalent access — ai kiểm soát được socket đó là kiểm soát được toàn bộ Docker trên máy. Một vài điểm cần nhớ:
- Không expose port 8080 trực tiếp ra internet — đặt sau Nginx reverse proxy với SSL, hoặc chặn bằng firewall chỉ cho phép IP CI/CD
- Token nên đủ dài và random:
openssl rand -hex 32là đủ - Private registry thì set credentials qua
~/.docker/config.jsonvà mount vào:-v $HOME/.docker/config.json:/config.json
Không phức tạp — đó là điểm mạnh
Watchtower không cố thay thế Kubernetes hay ArgoCD. Nó chỉ làm đúng một việc: tự động cái vòng lặp pull-stop-run mà bạn vẫn làm tay. Với homelab hay VPS cá nhân chạy vài chục container, đây là giải pháp rẻ nhất (free) và ít config nhất để có deploy tự động thực sự.
Setup hiện tại của mình: label-based để kiểm soát chính xác container nào được update, kết hợp HTTP API trigger từ GitHub Actions để update ngay sau khi push. Database và Redis không có label — Watchtower không bao giờ đụng vào. App code thì gắn label và để nó tự lo.
Từ hồi đó đến giờ chưa có thêm cái đêm nào thức đến 2 giờ sáng vì quên pull image nữa.
