Serve friendly error page when Fly.io proxy upstreams are unreachable #133

Merged
eblume merged 1 commit from feature/fly-proxy-error-page into main 2026-02-09 12:01:24 -08:00
4 changed files with 84 additions and 0 deletions
Showing only changes of commit d8960266b1 - Show all commits

Serve friendly error page when Fly.io proxy upstreams are unreachable

When indri is offline, the Tailscale tunnel is down, or the emergency
shutoff was triggered, visitors now see a branded 503 page instead of
nginx's default 502. Stale cache is still served when available
(proxy_cache_use_stale takes priority). Test endpoint at /_error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Erich Blume 2026-02-09 11:58:50 -08:00

View file

@ -0,0 +1 @@
Fly.io proxy serves a friendly error page when upstreams are unreachable (indri offline, Tailscale tunnel down, etc.). Test at `docs.eblu.me/_error`.

View file

@ -17,6 +17,7 @@ COPY --from=docker.io/grafana/alloy:v1.5.1 \
RUN mkdir -p /var/log/nginx /etc/alloy /tmp/alloy-data
COPY nginx.conf /etc/nginx/nginx.conf
COPY error.html /usr/share/nginx/html/error.html
COPY alloy.river /etc/alloy/config.alloy
COPY start.sh /start.sh
RUN chmod +x /start.sh

68
fly/error.html Normal file
View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Service Unavailable</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 480px;
text-align: center;
}
.status {
font-size: 4rem;
font-weight: 700;
color: #f97316;
line-height: 1;
margin-bottom: 1rem;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
}
p {
color: #94a3b8;
line-height: 1.6;
margin-bottom: 0.75rem;
}
.hint {
margin-top: 2rem;
padding: 1rem;
background: #1e293b;
border-radius: 8px;
font-size: 0.875rem;
color: #64748b;
}
.hint a {
color: #64748b;
text-decoration: underline;
text-decoration-color: #475569;
}
.hint a:hover {
color: #94a3b8;
}
</style>
</head>
<body>
<div class="container">
<div class="status">503</div>
<h1>Service Unavailable</h1>
<p>The upstream server is not reachable right now. This usually means
the backend is offline for maintenance or the network tunnel is down.</p>
<p>Services should recover automatically. Please try again shortly.</p>
<div class="hint"><a href="https://github.com/eblume/blumeops">BlumeOps</a> &mdash; eblu.me</div>
</div>
</body>
</html>

View file

@ -46,11 +46,25 @@ http {
limit_req zone=general burst=20 nodelay;
# Serve a friendly error page when upstreams are unreachable
# (indri offline, Tailscale tunnel down, emergency shutoff, etc.)
# proxy_cache_use_stale still takes priority when cached content exists.
error_page 502 503 504 /error.html;
location = /error.html {
root /usr/share/nginx/html;
internal;
}
location = /_error {
root /usr/share/nginx/html;
try_files /error.html =404;
}
location / {
set $upstream_docs https://docs.tail8d86e.ts.net;
proxy_pass $upstream_docs$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_intercept_errors on;
# Cache aggressively static site only.
# Do NOT use these settings for dynamic services.