182 lines
5.7 KiB
Markdown
182 lines
5.7 KiB
Markdown
# Cashow error pages
|
||
|
||
Branded static error pages served by Traefik in front of the LNbits instances,
|
||
replacing Traefik's default plain-text bodies (`no available server`,
|
||
`Internal Server Error`).
|
||
|
||
```
|
||
deploy/error-pages/
|
||
├── README.md you are here — not deployed
|
||
├── preview.html local review grid — not deployed
|
||
└── pages/ ← the ConfigMap is built from this directory only
|
||
├── 429.html
|
||
├── 500.html
|
||
├── 502.html
|
||
├── 503.html
|
||
├── 504.html
|
||
└── onderhoud.html
|
||
```
|
||
|
||
`README.md` and `preview.html` sit *outside* `pages/` on purpose: the ConfigMap
|
||
is built with `--from-file=.`, which would otherwise sweep them both in as keys.
|
||
|
||
## Design constraints
|
||
|
||
Each page is a single self-contained HTML file making **zero external requests** —
|
||
inline `<style>`, inline `<script>`, the logo inlined as a base64 `<image>` inside
|
||
an `<svg>`. This is not a preference. Traefik's `errors` middleware fetches only
|
||
the HTML document from the error service and serves that body on the *original*
|
||
domain, so any relative subresource (`/logo.svg`, `/style.css`) would resolve
|
||
against the LNbits instance that is currently down and 404.
|
||
|
||
System font stack throughout — no webfont, embedded or otherwise.
|
||
|
||
| file | size |
|
||
|---|---|
|
||
| `500.html` | 20.7 KB |
|
||
| `429.html`, `502.html`, `503.html`, `504.html`, `onderhoud.html` | 22.1 KB each |
|
||
| **total** | **131 KB** |
|
||
|
||
`500.html` is smaller because it has no retry block, and therefore ships no
|
||
countdown JS, no `<noscript>` refresh and none of the rail CSS.
|
||
|
||
About 16 KB of every page is the base64 logo. Budget is 1 MB (ConfigMap hard cap),
|
||
so there is room, but keep it in mind before adding anything per-page.
|
||
|
||
## Regenerate the ConfigMap
|
||
|
||
From `deploy/error-pages/pages`:
|
||
|
||
```bash
|
||
kubectl -n kube-system create configmap error-pages --from-file=. --dry-run=client -o yaml | kubectl apply -f -
|
||
```
|
||
|
||
The pod serving these mounts the ConfigMap; if it is mounted as a volume the
|
||
kubelet refreshes it within a minute or so, but restart the deployment if you
|
||
want the change immediately:
|
||
|
||
```bash
|
||
kubectl -n kube-system rollout restart deploy/error-pages
|
||
```
|
||
|
||
## Serving pod
|
||
|
||
The `errors` middleware needs a real backend to fetch pages from. Anything that
|
||
serves static files works; this is the smallest version.
|
||
|
||
```yaml
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: error-pages
|
||
namespace: kube-system
|
||
spec:
|
||
replicas: 1
|
||
selector:
|
||
matchLabels: { app: error-pages }
|
||
template:
|
||
metadata:
|
||
labels: { app: error-pages }
|
||
spec:
|
||
containers:
|
||
- name: nginx
|
||
image: nginxinc/nginx-unprivileged:alpine
|
||
ports:
|
||
- containerPort: 8080
|
||
volumeMounts:
|
||
- name: pages
|
||
mountPath: /usr/share/nginx/html
|
||
readinessProbe:
|
||
httpGet: { path: /503.html, port: 8080 }
|
||
volumes:
|
||
- name: pages
|
||
configMap:
|
||
name: error-pages
|
||
---
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: error-pages
|
||
namespace: kube-system
|
||
spec:
|
||
selector: { app: error-pages }
|
||
ports:
|
||
- port: 80
|
||
targetPort: 8080
|
||
```
|
||
|
||
## Middleware
|
||
|
||
```yaml
|
||
apiVersion: traefik.io/v1alpha1
|
||
kind: Middleware
|
||
metadata:
|
||
name: error-pages
|
||
namespace: kube-system
|
||
spec:
|
||
errors:
|
||
status:
|
||
- "429"
|
||
- "500-599"
|
||
service:
|
||
name: error-pages
|
||
port: 80
|
||
query: "/{status}.html"
|
||
```
|
||
|
||
`query` substitutes the numeric status directly, which is why the filenames must
|
||
be exactly `{status}.html`. A status in the range with no matching file (`501`,
|
||
`507`, …) gets a 404 from the error service and Traefik falls back to its own
|
||
plain body — so the six files cover the codes that actually occur, and anything
|
||
else degrades to today's behaviour rather than breaking.
|
||
|
||
Attach it to an instance ingress with the usual annotation:
|
||
|
||
```yaml
|
||
traefik.ingress.kubernetes.io/router.middlewares: kube-system-error-pages@kubernetescrd
|
||
```
|
||
|
||
> **Cross-namespace reference.** The middleware lives in `kube-system` while the
|
||
> instance ingresses live in their own namespaces. Traefik allows this by default,
|
||
> but if `providers.kubernetesCRD.allowCrossNamespace=false` is set the reference
|
||
> is silently ignored and you get the default error bodies back. Either enable it
|
||
> or copy the Middleware object into each instance namespace.
|
||
|
||
## No 404 page
|
||
|
||
Deliberate. Traefik does not run the middleware chain when no router matches, so
|
||
a 404 page would only ever fire for LNbits-generated 404s. Not worth the file.
|
||
|
||
## Planned maintenance
|
||
|
||
`onderhoud.html` is not wired to a status code — it is swapped in by hand.
|
||
Repoint the middleware query for the duration of the window:
|
||
|
||
```yaml
|
||
query: "/onderhoud.html"
|
||
```
|
||
|
||
Apply it, and every 5xx from the instances renders the maintenance page instead
|
||
of the per-status ones. Revert to `query: "/{status}.html"` when the window ends.
|
||
|
||
This only changes what is *rendered*. Traefik still returns the upstream status
|
||
code, so `onderhoud.html` goes out as a 503 for as long as the instances are
|
||
down — which is the correct signal for crawlers and uptime checks.
|
||
|
||
## Open TODOs
|
||
|
||
Both are marked in the HTML.
|
||
|
||
- **Logo.** Currently `www.cashow.be/img/cashow_extrasmall.png` base64-inlined —
|
||
a 250×250 raster, not a vector. Drop `assets/cashow-logo.svg` in the repo and
|
||
swap the marked `<!-- ==== CASHOW LOGO ==== -->` block in all six files; it is
|
||
byte-identical across them, so it is one find-and-replace.
|
||
- **Status host.** The ghost pill links to `https://status.cashow.be`, which did
|
||
not resolve as of 2026-08-17 (NXDOMAIN). Point it at the real status page or
|
||
remove the pill.
|
||
|
||
## Local review
|
||
|
||
Open `preview.html` in a browser to see all six side by side. It is a review aid
|
||
only and is not part of the ConfigMap.
|