Vũ Văn HảiFull-stack · AI-native
GuidesBlog
Discuss a project

© 2026 Vu Van Hai · Written from real deployment experience.

HomeGuidesBlogRSS
  1. Guides
  2. /VPS
  3. /Automatic maintenance page with Caddy during Docker deploys

Automatic maintenance page with Caddy during Docker deploys

Use Caddy's handle_errors to catch the 502 Bad Gateway that appears while a container rebuilds and serve a static maintenance page automatically, with no manual on/off switch.

Updated: Sep 21, 20265 min read
CaddyDockerVPS
On this page
  • Why this approach works well
  • Folder layout
  • 1. Create a standalone maintenance HTML page
  • 2. Mount the maintenance folder into the Caddy container
  • 3. Configure Caddy to catch the 502
  • 4. Restart Caddy to pick up the new volume mount
  • 5. Test the maintenance page
  • Sample maintenance.html (using the Tailwind CDN)
  • Troubleshooting

When you deploy an app with Docker behind Caddy as the reverse proxy, every docker compose up --build or container restart causes a short window of downtime. If someone visits during that window, Caddy cannot reach the container (the upstream) and returns its default 502 Bad Gateway - a blank, ugly page.

This guide intercepts that 502 and automatically shows a professional HTML maintenance page instead. Once the container is back up, Caddy sends requests to the real app again, with no action on your part.

It assumes Caddy already runs in Docker from ~/services/caddy (see Set up Caddy as a reverse proxy on a VPS with Docker). Replace example.com and my-app-container with your own domain and container name.

Why this approach works well

  1. A more professional user experience (UX): visitors know the system is being upgraded, instead of getting a bad impression or assuming the site is "down".
  2. Everything managed in one place: the Caddy config, the maintenance HTML file and docker-compose.yml all live in ~/services/caddy, which makes it easy to back up the config (to GitHub, for example) or move the whole stack to another VPS.
  3. Automated recovery: there is no maintenance switch to flip by hand (as with older Nginx setups). Caddy keeps retrying the proxy; as soon as the Docker build finishes and the container is available again, the 502 disappears and Caddy routes requests back to the site automatically and smoothly.
  4. Light on resources: you do not need a Blue-Green Deployment setup that eats VPS memory. It is a good fit for small apps, or any app that can tolerate a short maintenance window.

Folder layout

The Caddy folder on your server will look like this:

~/services/caddy/
+-- docker-compose.yml
+-- Caddyfile
+-- maintenance/
    +-- maintenance.html

1. Create a standalone maintenance HTML page

The maintenance page must be a standalone static HTML page, because the app server (Next.js, an API, etc.) is down at this point.

First, create a folder for the page inside the Caddy config folder (keeping everything managed in one place):

mkdir -p ~/services/caddy/maintenance

Next, create maintenance.html (copy the sample HTML at the end of this guide and paste it in):

nano ~/services/caddy/maintenance/maintenance.html

2. Mount the maintenance folder into the Caddy container

Caddy runs inside an isolated Docker container, so it cannot see folders on the host OS. You need to mount the folder into the container.

Open Caddy's docker-compose.yml:

nano ~/services/caddy/docker-compose.yml

Find the volumes section of the caddy service and add the line that mounts the maintenance folder:

services:
  caddy:
    image: caddy:latest # or alpine, whichever you use
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      # The crucial line: mount the host folder as Caddy's root folder
      - ./maintenance:/var/www/maintenance
      - caddy_data:/data
      - caddy_config:/config

3. Configure Caddy to catch the 502

Open the Caddyfile:

nano ~/services/caddy/Caddyfile

Add a handle_errors block inside your domain block. Here is a complete config for example.com:

example.com {
    # 1. Send normal traffic to your Docker app (frontend/API)
    reverse_proxy my-app-container:3000

    # 2. This block handles errors coming from reverse_proxy or from Caddy itself
    handle_errors {
        # Match only 502 (Bad Gateway - upstream timeout, container down or restarting)
        @502 {
            expression {err.status_code} == 502
        }

        # When the 502 matcher hits, serve the static file with this block:
        handle @502 {
            # Point Caddy at the static folder mounted in step 2
            root * /var/www/maintenance
            # Rewrite every failed request to this one HTML file
            rewrite * /maintenance.html
            # Caddy acts as a file server and returns the file to the client
            file_server
        }
    }
}

4. Restart Caddy to pick up the new volume mount

Because you just edited docker-compose.yml (to mount a new volume), you need to recreate the Caddy container entirely, rather than running the usual soft config reload.

cd ~/services/caddy
docker compose up -d caddy
# Or, to be safer: docker compose down && docker compose up -d

5. Test the maintenance page

You can now simulate maintenance right on the host:

  1. Stop the main app (the frontend, for example): docker stop my-app-container.
  2. Refresh the site in your browser: instead of the default 502 page, you get the HTML maintenance page in the colors of your own brand design.
  3. Bring the app back with docker compose up -d. Refresh a few seconds later and the site works normally again.

Sample maintenance.html (using the Tailwind CDN)

So the site still looks good during maintenance, copy this HTML into maintenance.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>System Under Maintenance</title>
    <!-- Load Tailwind from the CDN for quick styling -->
    <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 / artwork - swap in your project's own -->
        <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">
            The System Is Being Upgraded
        </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>The system is currently undergoing automated maintenance and a data update. Please wait, all services are expected to be back within a few minutes.</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">
                Reload Page
            </button>
        </div>
    </div>
</body>
</html>

Troubleshooting

SymptomFix
Still seeing the blank 502 page after editing the CaddyfileThe maintenance folder is not mounted into the container - check the volume line from step 2, then recreate the container with docker compose up -d caddy; a caddy reload alone is not enough
The maintenance page shows up but looks brokenThe page must be standalone static HTML: load CSS/JS from a CDN or inline it, never from the app that is down
The maintenance page never goes awayThe app container is not back up - Caddy only routes requests to the app once the upstream is available, so check the app's logs
PreviousSet up Caddy as a reverse proxy on a VPS with DockerNextQuick-check a VPS's specs with a single command

Related articles

  • Where to put apps on a VPS: the /opt/apps directory layout

    A simple convention for app code on a VPS: keep each app in its own folder under /opt/apps, chown the parent folder once, and git clone, git pull and .env edits never need sudo again.

    VPS

    VPS
  • Roll back a Git commit and update safely on a VPS

    Take your project back to an older commit, force push to the remote, then reset the VPS to match without a merge conflict, then rebuild the service and clear caches.

    Dev tools

    Dev tools
  • Fix Docker containers that cannot reach PostgreSQL on a VPS

    Docker picks a new network range every time you run docker-compose down and up, so PostgreSQL on the host rejects the container. Diagnose the subnet, open UFW and pg_hba.conf, then allow the whole 172.16.0.0/12 range to fix it once.

    Database

    Database

Written by Vu Van Hai

I'm Hai, a full-stack developer based in Ho Chi Minh City. These guides come from systems I built and run myself. Need to build or untangle something similar? Get in touch.

Discuss a projectMore guides

Spot a mistake or a command that no longer works? Let me know

On this page

  • Why this approach works well
  • Folder layout
  • 1. Create a standalone maintenance HTML page
  • 2. Mount the maintenance folder into the Caddy container
  • 3. Configure Caddy to catch the 502
  • 4. Restart Caddy to pick up the new volume mount
  • 5. Test the maintenance page
  • Sample maintenance.html (using the Tailwind CDN)
  • Troubleshooting