Trang bảo trì tự động với Caddy khi deploy Docker
Dùng handle_errors của Caddy để bắt lỗi 502 Bad Gateway lúc container đang rebuild và tự động hiển thị một trang HTML bảo trì, không cần bật tắt thủ công.
Mục lục
Khi bạn triển khai ứng dụng bằng Docker và dùng Caddy làm reverse proxy, mỗi lần chạy
docker compose up --build hoặc khởi động lại container, ứng dụng sẽ có một khoảng
downtime ngắn. Nếu người dùng truy cập đúng lúc đó, Caddy không kết nối được tới
container (upstream) và trả về lỗi 502 Bad Gateway mặc định - một trang trắng xóa,
rất xấu.
Bài này giúp bạn "đánh chặn" lỗi 502 đó và tự động hiển thị một trang HTML bảo trì (maintenance page) chuyên nghiệp. Khi container chạy xong, Caddy tự động đưa request về lại ứng dụng chính như bình thường.
Bài giả định Caddy đã chạy bằng Docker trong ~/services/caddy (xem
Cài Caddy làm reverse proxy trên VPS bằng Docker).
Thay example.com và my-app-container bằng domain và tên container của bạn.
Lợi ích của cách làm này
- Trải nghiệm người dùng (UX) chuyên nghiệp hơn: người dùng biết hệ thống đang nâng cấp, tránh ấn tượng xấu hoặc tưởng nhầm website đã "sập".
- Quản lý tập trung một chỗ: toàn bộ file cấu hình Caddy, file HTML bảo trì và
docker-compose.ymlđều nằm gọn trong thư mục~/services/caddy, thuận tiện cho việc backup cấu hình (ví dụ lên GitHub) hoặc chuyển nguyên cụm sang VPS khác. - Tự phục hồi (automated recovery): không cần bật/tắt trang bảo trì thủ công (như kiểu Nginx cũ). Caddy liên tục tự retry proxy; ngay khi Docker build xong và container khả dụng lại, lỗi 502 tự biến mất và Caddy nối request về lại website một cách tự động, mượt mà.
- Tối ưu tài nguyên: không cần các giải pháp Blue-Green Deployment tốn bộ nhớ VPS, hoàn toàn phù hợp với ứng dụng nhỏ hoặc ứng dụng chịu được một khoảng downtime bảo trì ngắn.
Cấu trúc thư mục
Thư mục Caddy trên máy chủ của bạn sẽ trông như sau:
~/services/caddy/
+-- docker-compose.yml
+-- Caddyfile
+-- maintenance/
+-- maintenance.html1. Tạo trang HTML bảo trì độc lập
Trang bảo trì phải là một trang HTML tĩnh độc lập, vì lúc này app server (ví dụ Next.js, API) đang tắt.
Đầu tiên, tạo thư mục chứa trang bảo trì ngay trong thư mục cấu hình Caddy (giữ nguyên tắc quản lý tập trung):
mkdir -p ~/services/caddy/maintenanceTiếp theo, tạo file maintenance.html (copy mã HTML mẫu ở cuối bài rồi dán vào):
nano ~/services/caddy/maintenance/maintenance.html2. Mount thư mục maintenance vào container Caddy
Caddy đang chạy trong một container Docker cô lập nên không nhìn thấy thư mục bên ngoài host OS. Vì vậy ta cần mount (gắn) thư mục đó vào trong container.
Mở file docker-compose.yml của Caddy:
nano ~/services/caddy/docker-compose.ymlTìm đến phần volumes của service caddy và thêm dòng mount thư mục maintenance:
services:
caddy:
image: caddy:latest # hoặc alpine tùy bạn đang dùng
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
# Dòng cực kỳ quan trọng: gắn thư mục host vào thư mục root của Caddy
- ./maintenance:/var/www/maintenance
- caddy_data:/data
- caddy_config:/config3. Cấu hình Caddy bắt lỗi 502
Mở file Caddyfile:
nano ~/services/caddy/CaddyfileThêm khối handle_errors vào bên trong block domain của bạn. Dưới đây là cấu hình
hoàn chỉnh cho example.com:
example.com {
# 1. Chuyển traffic bình thường vào Docker app (frontend/API) của bạn
reverse_proxy my-app-container:3000
# 2. Khối này xử lý các lỗi trả ra từ reverse_proxy hoặc từ chính Caddy
handle_errors {
# Bắt riêng lỗi 502 (Bad Gateway - upstream timeout, container đang tắt hoặc khởi động lại)
@502 {
expression {err.status_code} == 502
}
# Nếu đã bắt trúng lỗi 502, trả về file tĩnh bằng khối lệnh sau:
handle @502 {
# Báo cho Caddy trỏ đến thư mục tĩnh đã mount ở bước 2
root * /var/www/maintenance
# Điều hướng mọi request lỗi về đúng file HTML này
rewrite * /maintenance.html
# Caddy đóng vai trò file server, trả file cho client
file_server
}
}
}4. Khởi động lại Caddy để nhận volume mount mới
Vì vừa sửa file docker-compose.yml (để mount volume mới), bạn cần tạo lại hoàn toàn
container Caddy, thay vì chỉ gõ lệnh reload cấu hình mềm như thông thường.
cd ~/services/caddy
docker compose up -d caddy
# Hoặc an toàn hơn: docker compose down && docker compose up -d5. Kiểm thử trang bảo trì
Giờ bạn có thể giả lập bảo trì ngay trên host:
- Dừng ứng dụng chính (ví dụ frontend):
docker stop my-app-container. - Mở trình duyệt và refresh trang: web không còn hiện trang 502 mặc định nữa, mà hiện trang HTML bảo trì với màu sắc đúng nhận diện thiết kế của bạn.
- Bật lại ứng dụng bằng lệnh
docker compose up -d. Vài giây sau refresh lại, web hoạt động bình thường trở lại.
Mẫu maintenance.html (dùng Tailwind CDN)
Để web vẫn đẹp cả khi đang bảo trì, copy đoạn HTML sau vào file maintenance.html:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Đang Bảo Trì Hệ Thống</title>
<!-- Nạp Tailwind qua CDN để có style nhanh gọn -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@300;400;500;600;700&display=swap');
body { font-family: 'Be Vietnam Pro', sans-serif; background-color: #FAFAFA; }
.text-primary { color: #D4AF37; }
.bg-primary { background-color: #D4AF37; }
.halo-effect { box-shadow: 0 0 50px rgba(212, 175, 55, 0.15); }
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-2xl w-full bg-white rounded-2xl shadow-sm border border-gray-100 p-8 md:p-12 text-center halo-effect">
<!-- Logo / họa tiết - thay bằng của dự án bạn -->
<div class="flex justify-center mb-8">
<svg class="w-16 h-16 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 2v20m-9.6-9.6l19.2 0m-17.5-6.5l15.8 13.1m-15.8 0l15.8-13.1 M12 22c5.522 0 10-4.478 10-10S17.522 2 12 2 2 6.478 2 12s4.478 10 10 10z"></path>
</svg>
</div>
<h1 class="text-3xl md:text-3xl font-bold text-gray-800 mb-4 tracking-tight">
Hệ Thống Đang Trải Qua Nâng Cấp
</h1>
<p class="text-gray-500 mb-2 font-medium">(System is under maintenance)</p>
<div class="h-px bg-gray-100 w-24 mx-auto my-6"></div>
<div class="space-y-4 text-gray-600">
<p>Hệ thống hiện đang bảo trì tự động và cập nhật dữ liệu. Xin vui lòng chờ, mọi dịch vụ dự kiến sẽ hoạt động lại trong vòng một vài phút nữa.</p>
</div>
<div class="mt-10">
<button onclick="window.location.reload()" class="bg-primary hover:bg-[#C5A030] text-white font-medium py-3 px-8 rounded-full transition duration-300 shadow-md">
Tải Lại Trang (Reload Page)
</button>
</div>
</div>
</body>
</html>Lưu ý
| Triệu chứng | Cách xử lý |
|---|---|
| Vẫn thấy trang 502 trắng sau khi sửa Caddyfile | Thư mục maintenance chưa được mount vào container - kiểm tra dòng volume ở bước 2 rồi tạo lại container bằng docker compose up -d caddy; chỉ caddy reload là không đủ |
| Trang bảo trì hiện ra nhưng vỡ giao diện | Trang phải là HTML tĩnh độc lập: CSS/JS lấy từ CDN hoặc nhúng inline, không phụ thuộc vào app đang tắt |
| Trang bảo trì không tự biến mất | Container ứng dụng chưa chạy lại - Caddy chỉ nối request về app khi upstream khả dụng, hãy kiểm tra logs của app |