Out of the box, when a container is down (Sim crashed, Ollama OOM'd, etc.) Caddy returns a bare 502 page. When you typo a URL on the apex (pocketcode.in/dahsboard), the gateway 404s. Neither is helpful or fun. v1.4 adds a single themed error page that handles both, with the service's logo, a humor line, and a live console log of what Caddy actually saw.
🪄One page, two roles. auth-gateway/public/error.html reads its query string client-side and switches between two modes: /error?lost=1&from=<path> (friendly 404) and /error?service=<name>&code=<n> (service-down splash with that service's logo + color). The page is served by the gateway's /error route, which is public (no auth gate) so it's reachable even before login.
Part A: Verify the route & page exist.
# Both files were extracted by Tab 15 Step 1:
ls -la ~/ai-stack/auth-gateway/public/error.html
grep "app.get('/error'" ~/ai-stack/auth-gateway/server.js
grep "app.use((req, res) => {" ~/ai-stack/auth-gateway/server.js # the catch-all
# Rebuild gateway so the route + page land in the image:
bash ~/ai-stack/auth-gateway/run-auth.sh
# Smoke test the public /error route directly (no auth needed):
curl -sI https://pocketcode.in/error?lost=1 | head -3
# Expect: HTTP/2 404 (lost mode sets a 404 status)
curl -sI "https://pocketcode.in/error?service=sim&code=502" | head -3
# Expect: HTTP/2 502 (service-down sets the service's status)
# And the catch-all → unknown apex paths bounce to /error:
curl -sI https://pocketcode.in/this-route-does-not-exist | head -3
# Expect: HTTP/2 302 Location: /error?lost=1&from=%2Fthis-route-...
Part B: Add handle_errors to each gated subdomain in the Caddyfile. This is what catches a container being down and serves the friendly page instead of Caddy's bare 502. Open the Caddyfile once and make 9 edits — same shape each time, only the service name changes.
nano /root/ai-stack/caddy/Caddyfile
For each of the 9 sites below, do this 4-step loop in nano:
- Press Ctrl+W, paste the Find string, press Enter — nano jumps to the opening line of that site block.
- Press Down arrow repeatedly until you hit a line that's just
} at column 0 (no leading spaces). That's the outermost closing brace. Inner }s for nested handle blocks are indented with 4 spaces, so they don't count.
- Once cursor is on that
} line, press Home, then Enter — this pushes } down one line and leaves a blank line above. Press Up to land on that blank line.
- Paste the block for that site, exactly as shown (indentation is part of it).
When all 9 are done, save with Ctrl+O, Enter, Ctrl+X.
Edit 1 — sim
handle_errors {
rewrite * /error?service=sim&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 2 — chat
handle_errors {
rewrite * /error?service=chat&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 3 — n8n
handle_errors {
rewrite * /error?service=n8n&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 4 — openclaw
handle_errors {
rewrite * /error?service=openclaw&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 5 — ollama
handle_errors {
rewrite * /error?service=ollama&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 6 — openrouter
openrouter.pocketcode.in {
handle_errors {
rewrite * /error?service=openrouter&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 7 — qdrant
handle_errors {
rewrite * /error?service=qdrant&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 8 — pgadmin
handle_errors {
rewrite * /error?service=pgadmin&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Edit 9 — terminal
handle_errors {
rewrite * /error?service=terminal&code={err.status_code}
reverse_proxy auth-gateway:7000
}
Sanity-check before reloading. Don't reload yet — first confirm the file is syntactically valid and that the new block landed in the right place 9 times:
grep -c 'handle_errors {' /root/ai-stack/caddy/Caddyfile
Expected output: 9. If you see anything else, you missed a site or pasted twice into one — reopen and fix before continuing.
docker exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
Expected last line: Valid configuration. If validation fails, the error message names the line number — fix in nano and re-validate.
Reload Caddy. Zero downtime; running connections are not affected.
docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
A successful reload prints nothing and returns to the prompt.
⚠️Do NOT add handle_errors to the apex (pocketcode.in) block. The apex reverse-proxies to the auth-gateway itself, and if the gateway is down, the handle_errors rewrite would try to proxy to the same dead gateway — infinite failure. The gateway's own Express catch-all already handles unknown apex paths via /error?lost=1. If the gateway itself is down, you get a bare Caddy 502 on the apex — at which point ai-doctor on the host will tell you exactly what to fix.
📍Why each block needs its own line, not a snippet: Caddyfile's handle_errors directive uses placeholders that resolve at request time, but the service=<name> param is fixed per site block. You could DRY this up with a Caddy snippet that takes an argument, but the explicit form makes the routing obvious when debugging.
Part C: Test the service-down splash. Stop a container and verify the friendly page renders. The easiest test is in your browser — you're already logged in there, so the cookie travels automatically and Caddy actually reaches the (now-down) upstream, triggering handle_errors.
# Stop sim-simstudio-1 temporarily:
docker stop sim-simstudio-1
# In a browser, visit https://sim.pocketcode.in/
# Expected: purple Sim.ai-themed splash with "Sim.ai is taking a nap.",
# console log of the failed connection, URL stays sim.pocketcode.in.
# Bring sim back up:
docker start sim-simstudio-1
⚠️Naked curl will return 302, not 502 — and that's correct behavior, not a bug. Without the session cookie, auth_gate fires first (sees no cookie → 401 → Caddy 302s to /login?return=...). The request never reaches the reverse_proxy line, so the upstream-down condition that triggers handle_errors never happens. To curl-test the error page, you need to send the gateway cookie too.
Curl test loop with session cookie. First, log in via curl and save the cookie to a temp file. Run this on the server:
# 1. Look up your gateway username (in case it isn't "admin")
USERNAME=$(grep '^AUTH_USERNAME=' ~/ai-stack/auth-gateway/.env | cut -d= -f2)
echo "username: $USERNAME"
# 2. Prompt for your gateway password without echoing it to the terminal
read -rs -p "Gateway password for ${USERNAME}: " PW; echo
# 3. Log in and capture the cookie to /tmp/pc.cookies
curl -s -c /tmp/pc.cookies \
-X POST https://pocketcode.in/login \
--data-urlencode "username=${USERNAME}" \
--data-urlencode "password=${PW}" \
-o /dev/null
# 4. Verify we got the cookie
if grep -q pocketcode_session /tmp/pc.cookies; then
echo "✓ session cookie captured"
else
echo "✗ login failed — check username/password and retry"
fi
unset PW
Now the test loop — stops each service, hits its subdomain with the cookie, asserts the error page rendered, restarts the service:
for svc in sim:sim-simstudio-1 chat:open-webui n8n:n8n openclaw:openclaw \
ollama:ollama openrouter:openrouter-proxy qdrant:qdrant \
pgadmin:pgadmin; do
sub="${svc%%:*}"
cont="${svc##*:}"
echo "── testing $sub (container: $cont) ──"
docker stop "$cont" >/dev/null
sleep 2
code=$(curl -s -b /tmp/pc.cookies -o /dev/null -w "%{http_code}" "https://${sub}.pocketcode.in/")
body=$(curl -s -b /tmp/pc.cookies "https://${sub}.pocketcode.in/" | grep -o 'service=[a-z]*' | head -1)
echo " HTTP code: $code · error.html params: $body"
docker start "$cont" >/dev/null
sleep 3
done
rm -f /tmp/pc.cookies
Expected per site: HTTP code: 502 (or 503 for transient unhealthy state) and error.html params: service=<sub>. Both prove handle_errors caught the upstream failure and rewrote to the gateway's /error page with the right service param.
The terminal site is skipped from the loop because ttyd is a host systemd service, not a container. Test it manually:
sudo systemctl stop ttyd
sleep 2
curl -s -b /tmp/pc.cookies -o /dev/null -w "%{http_code}\n" https://terminal.pocketcode.in/
# Expected: 502
sudo systemctl start ttyd
Part D: Test the lost (404) page. Just hit any nonexistent apex path:
# Browser:
# Visit https://pocketcode.in/whoknows — you should see the 404
# compass icon, "You are lost." headline, and one of the random
# humor lines. Press R to retry (goes home), H to home directly.
curl -sIL https://pocketcode.in/whoknows | grep -E '^(HTTP|location)'
# Expect: HTTP/2 302 → Location: /error?lost=1&from=%2Fwhoknows
# HTTP/2 404 (the error page itself)
Part E: Add error-page checks to diagnose.sh. Open ~/ai-stack/manage/diagnose.sh and find the "Auth Gateway" section. Add two lines after the existing /sso-services check:
# /error?lost=1 should serve the page with 404 status
err_lost=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "https://pocketcode.in/error?lost=1" 2>/dev/null)
if [ "$err_lost" = "404" ]; then
echo -e " ${G}✓${NC} /error?lost=1 serves friendly 404 (HTTP 404)"
else
echo -e " ${Y}⚠${NC} /error?lost=1 returned HTTP $err_lost (expected 404) — rebuild gateway"
fi
# Catch-all → unknown apex path redirects to /error?lost=1
catchall=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "https://pocketcode.in/this-does-not-exist" 2>/dev/null)
if [ "$catchall" = "302" ]; then
echo -e " ${G}✓${NC} unknown apex path redirects to /error (HTTP 302)"
else
echo -e " ${Y}⚠${NC} unknown apex path returned HTTP $catchall (expected 302)"
fi
# error.html present in container
if docker exec auth-gateway test -f /app/public/error.html 2>/dev/null; then
echo -e " ${G}✓${NC} error.html present in gateway container"
else
echo -e " ${R}✗${NC} error.html MISSING — rebuild gateway:"
echo -e " bash ~/ai-stack/auth-gateway/run-auth.sh"
fi
🎨Customizing the humor. Open ~/ai-stack/auth-gateway/public/error.html and find the SERVICES JS object near the bottom — each service has a humor string and the LOST_LINES array right below holds the 404 jokes. Tweak to taste, then bash ~/ai-stack/auth-gateway/run-auth.sh to rebuild. The page also responds to keyboard shortcuts: R = retry, H = home.