Vũ Văn HảiFull-stack · AI-native
Hướng dẫnBlog
Trao đổi dự án

© 2026 Vũ Văn Hải · Viết từ kinh nghiệm triển khai thật.

Trang chủHướng dẫnBlogRSS
  1. Hướng dẫn
  2. /VPS
  3. /Trang bảo trì tự động với Caddy khi deploy Docker

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.

Cập nhật: 21 thg 9, 20266 phút đọc
CaddyDockerVPS
Mục lục
  • Lợi ích của cách làm này
  • Cấu trúc thư mục
  • 1. Tạo trang HTML bảo trì độc lập
  • 2. Mount thư mục maintenance vào container Caddy
  • 3. Cấu hình Caddy bắt lỗi 502
  • 4. Khởi động lại Caddy để nhận volume mount mới
  • 5. Kiểm thử trang bảo trì
  • Mẫu maintenance.html (dùng Tailwind CDN)
  • Lưu ý

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

  1. 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".
  2. 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.
  3. 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à.
  4. 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.html

1. 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/maintenance

Tiế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.html

2. 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.yml

Tì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:/config

3. Cấu hình Caddy bắt lỗi 502

Mở file Caddyfile:

nano ~/services/caddy/Caddyfile

Thê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 -d

5. Kiểm thử trang bảo trì

Giờ bạn có thể giả lập bảo trì ngay trên host:

  1. Dừng ứng dụng chính (ví dụ frontend): docker stop my-app-container.
  2. 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.
  3. 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ứngCách xử lý
Vẫn thấy trang 502 trắng sau khi sửa CaddyfileThư 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ệnTrang 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ấtContainer ứ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
Bài trướcCài Caddy làm reverse proxy trên VPS bằng DockerBài tiếp theoKiểm tra nhanh thông số VPS bằng một dòng lệnh

Bài liên quan

  • Đặt app ở đâu trên VPS? Cấu trúc thư mục chuẩn /opt/apps

    Quy ước đặt mỗi app trong một thư mục con của /opt/apps trên VPS: chown thư mục cha một lần duy nhất, từ đó git clone, git pull và sửa .env đều không cần sudo.

    VPS

    VPS
  • Quay ngược commit Git và cập nhật an toàn trên VPS

    Cách đưa dự án về một commit cũ, force push lên remote, rồi reset code trên VPS cho khớp mà không dính merge conflict, sau đó rebuild service và xử lý cache.

    Công cụ dev

    Công cụ dev
  • Sửa lỗi Docker container không kết nối được PostgreSQL trên VPS

    Docker đổi dải IP mạng mỗi lần docker-compose down rồi up, nên PostgreSQL trên host từ chối container. Cách chẩn đoán subnet, mở UFW và pg_hba.conf, rồi cho phép cả dải 172.16.0.0/12 để sửa một lần là xong.

    Database

    Database

Viết bởi Vũ Văn Hải

Tôi là Hải, full-stack developer ở TP. Hồ Chí Minh. Các bài ở đây đúc kết từ những hệ thống tôi tự dựng và vận hành. Cần dựng hoặc gỡ rối một hệ thống tương tự? Cứ nhắn tôi.

Trao đổi dự ánXem thêm hướng dẫn

Thấy sai sót hoặc lệnh không còn chạy? Báo cho tôi

Mục lục

  • Lợi ích của cách làm này
  • Cấu trúc thư mục
  • 1. Tạo trang HTML bảo trì độc lập
  • 2. Mount thư mục maintenance vào container Caddy
  • 3. Cấu hình Caddy bắt lỗi 502
  • 4. Khởi động lại Caddy để nhận volume mount mới
  • 5. Kiểm thử trang bảo trì
  • Mẫu maintenance.html (dùng Tailwind CDN)
  • Lưu ý