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:
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:
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.
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
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:
traefik.ingress.kubernetes.io/router.middlewares: kube-system-error-pages@kubernetescrd
Cross-namespace reference. The middleware lives in
kube-systemwhile the instance ingresses live in their own namespaces. Traefik allows this by default, but ifproviders.kubernetesCRD.allowCrossNamespace=falseis 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:
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.pngbase64-inlined — a 250×250 raster, not a vector. Dropassets/cashow-logo.svgin 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.