Dashy
The homelab homepage. One card per service, grouped by what the service is for rather than by which host it happens to run on.
This is not a Compose stack. Dashy runs natively in its own LXC, installed from the
community-scripts helper. The directory
lives here anyway so the dashboard content sits alongside the rest of the service
inventory — but there is no docker-compose.yaml, and docker compose commands do not
apply to it.
| Guest | CT 270 dashy on pve-dell, unprivileged |
| Address | 192.168.1.25, port 4000 |
| Version | Dashy 4.5.0 |
| Service | dashy.service — /usr/bin/node server.js |
| Working dir | /opt/dashy |
| Config | /opt/dashy/user-data/conf.yml |
| Resources | 2 cores, 2 GB RAM, 6 GB rootfs, onboot: 1 |
- Internal: https://dashy.toolsera.lan/ — Step CA cert, no forward auth
- Public: https://dashy.toolsera.fr/ — Let’s Encrypt via the tunnel, behind Authelia
- Direct: http://192.168.1.25:4000/ — bypasses Traefik entirely
The dashboard content
Section titled “The dashboard content”user-data/conf.yml is the canonical copy and the one to edit.
It holds 86 services across 11 sections: Control Plane, Kubernetes · prod-talos,
Observability, Identity & Certificates, Registries & Code, Network & Access, Knowledge &
Documents, Media, Media Automation, Downloads, Automation.
The service list was taken from Traefik’s live router table, not from compose labels
— on this fleet those describe only a fraction of what is routed. Every hostname was
HTTP-checked before being added, and every hl- icon slug was checked against the
dashboard-icons repo so none render broken.
Boxarr, CrossWatch and GameVault come from the docker-apps-cifs stack, documented
in
../../docker-compose-apps/docker-apps-cifs/README.md.
When a tile is added before its backend, note that 404 means no route while 502 means
routed with no backend — the wildcard zone resolves either way.
The board covers three sources. Most tiles are Proxmox guests behind the LXC
Traefik. Nine come from the prod-talos Kubernetes cluster, reconciled by Flux from
the fleet-infra repo — that cluster runs
its own Traefik and its own Cloudflare tunnel. Only workloads actually listed in a
cluster kustomization were added:
| Tile | Source in fleet-infra |
|---|---|
| Kite, Dozzle (Talos), whoami | apps/prod-talos-cluster/ |
| Traefik (Talos) | apps/prod-talos-cluster/traefik/ |
| Goldilocks | infrastructure/prod-talos-cluster/goldilocks/ |
| Grafana, Prometheus, Alertmanager | infrastructure/.../kube-prometheus-stack/ |
| Dex | apps/prod-talos-cluster/dex/ |
Harbor and Nexus were already on the board and also run on that cluster — the
.toolsera.lan hostnames they were added under are its IngressRoutes.
Four things about that cluster shaped what went on the board:
- Manifests are not deployments.
awx,headlamp,otf,otf-agent,terrakubeandweaveall have manifests in the repo but are commented out of their kustomizations, so nothing runs behind them.awx.toolsera.lanreturns 404, andheadlamp.toolsera.frreturns a Cloudflare Access redirect from an Access app that outlived its backend — which is exactly why a 302 on a.frhost proves nothing. - Gateway API routes were not duplicated. Every one of these apps also publishes an
HTTPRoute on a parallel
*-gwhostname reaching the same backend. Adding both would have doubled the section for nothing. - Two tiles are namesakes. The cluster’s Traefik and Dozzle would be indistinguishable from the fleet’s if they were filed by purpose, so the cluster keeps its own section. Grafana, Prometheus, Alertmanager and Dex have no such twin and are filed by purpose, in Observability and Identity.
- Dex has no UI.
/and/dex/both 404; only/dex/authand the discovery document answer. Its tile links to the discovery document.
The third source is the UGREEN NAS (192.168.1.29), whose stack lives in
../../docker-compose-apps/ugreen-nas/. It is
not a Proxmox guest, so the provider plugin below cannot discover its address — and yet
its seven services are routed from Proxmox notes like everything else, because
loadbalancer.server.url overrides address discovery outright. They live in VM 217
docker-apps-cifs’s description as …server.url=http://192.168.1.29:<port>; the
guest whose notes carry a route need not be the guest that answers it. Details in
../../docker-compose-apps/traefik/dynamic/README.md.
One thing to keep in mind: those ports are duplicated state, mirroring the NAS
stack’s docker-compose.yml. Republishing an app on a different port there breaks its
route with nothing to catch it — which is exactly what happened to Arr Dashboard when it
moved to 49153.
Three NAS apps are deliberately not on the board: metube, speedtest-tracker and
upsnap. The NAS runs its own copies, but those three hostnames already resolve to the
LXC instances (192.168.1.21, .185, .196) through the Proxmox provider. Two tiles
differing only by backend IP is worse than one, so the NAS copies stay IP-only.
Media is deliberately two sections, and Downloads is a third. Routing the
natively-installed guests added nine media tiles, and a single section ran to fourteen —
long past the point where a grid is faster to scan than a list. It is split by what you
are actually there to do: Media serves or plays the library, Media Automation fills
it. The two share a hue so they still read as one family. Downloads — qBittorrent,
qui, qbitwebui, Rustatio, Cleanuparr, Gluetun — was split off for the same reason once
the NAS and CIFS apps landed: it is the layer that moves the bytes, and folding it into
Media Automation would have put that section back at sixteen. It is also where five of
the six IP-linked NAS tiles live, so the one section that behaves differently is the one
section you can point at.
Where a service publishes both routes it is linked at .toolsera.lan, which skips the
Cloudflare tunnel hop. That is why the cluster’s Traefik is linked at
traefik-prod-talos.toolsera.lan/dashboard/ rather than at its .fr twin. Eight tiles
are linked at .toolsera.fr: Keycloak, Authelia, and the six cluster ones — Kite, Dozzle
(Talos), Goldilocks, whoami, Grafana and Dex.
Readeck, Excalidash, Remmina, Seerr, Tube Archivist, n8n and Traefik Manager moved from
that list to .lan once their LAN routers existed.
When adding a .lan twin, give it secure-chain-no-auth@file. Copying the public
router’s secure-chain-auth@file puts Authelia in front of a route that never leaves the
LAN: it still works in a browser, but the status check goes red.
One deliberate omission, verified:
terrakubeis listed in../../CLAUDE.mdas an internal domain but has no router. The zone wildcards to Traefik, so it resolves and returns 404. It has manifests infleet-infratoo, also not deployed.
opengist was listed here too while its backend 502’d. It answers 302 as of 2026-08-23,
so its tile is green again and the note is gone.
The 2026-08-23 batch
Section titled “The 2026-08-23 batch”Four guests changed under the board and are worth reading together, because three of the four problems were the guest description, not the service.
- PegaProx is gone. CT 268 was destroyed on 2026-08-20. Its tile and its file-provider
route (
dynamic/pegaprox.yml) both outlived it, sopegaprox.toolsera.lanresolved and answered 502 for three days. Both were removed together — see../../docker-compose-apps/traefik/dynamic/README.mdfor why that route needed a file in the first place, which is still true of anything serving HTTPS with a certificate Traefik will not verify. prowlarrwas already on the board and had silently stopped routing. CT 230 was destroyed and recreated on 2026-08-19, and the rebuild came back with the stock community-script description — so the label block went with it and the tile started 404ing. A recreated guest loses its routing, and nothing reports it. The board tile survives the rebuild; the route does not.- Three new LXCs —
bookorbit(CT 208, :3000),komga(CT 236, :25600) andautobrr(CT 258, :7474) — had the same stock description and had never been routed. All four got the standard-lanblock appended, mirroringradarr’s, and all four answered within the plugin’s 30-second poll: 200, 200, 200, and 302 for Prowlarr’s login redirect. comicarris the inverse case: routed indocker-apps-cifs.ymlon 2026-08-22 with no container ever deployed on VM 217. Its tile is on the board at the user’s request and shows red at 502 until the service comes up. A route is not a deployment.
The 2026-08-23 unrouted sweep added five more. Comparing Traefik’s service backends
against every running LXC by IP — names lie, remmina-kasm serves remmina — turned
up 17 guests nothing routed to. Most are deliberate (no UI, an API, or joplin-server,
which validates the Host header), but five were running a web UI with no label block, so
neither this board nor Raindrop knew they existed: radarr-anime (CT 254 :7878),
sftpgo (CT 214 :8080), garage-web-ui (CT 257 :3909), cronmaster (CT 260 :40123)
and filebrowser (CT 260 :5436). All five are routed and tiled now.
ProxMenux is the first tile for something on the node itself. It runs on pve-dell
(:8008), not in a guest, and the provider plugin reads labels out of guest
descriptions — the node has none, so no notes-based route is possible. It is defined in
../../docker-compose-apps/traefik/dynamic/proxmenux.yml
instead. Anything else installed on the node will need the same treatment.
cronmaster and filebrowser share CT 260 — one guest, two label blocks, two tiles,
filed apart by what they are for. That is the pattern for any guest serving more than one
thing.
Two other guests were destroyed in the same window — huntarr (CT 216) and ollama
(CT 229), both on 2026-08-22. Neither was ever routed, so neither had a tile and nothing
here changed.
Updating the dashboard
Section titled “Updating the dashboard”Edit user-data/conf.yml here, commit, then push it to the guest. Most LXCs on this
fleet refuse SSH, so go through the node with pct push:
# from a checkout on the Proxmox nodepct push 270 user-data/conf.yml /opt/dashy/user-data/conf.ymlpct exec 270 -- systemctl restart dashyDashy’s own UI can write config back to that file. That copy then diverges from this
one with nothing to reconcile it, so treat UI edits as a scratchpad and fold anything
worth keeping back into conf.yml here.
Routing
Section titled “Routing”Routers on this fleet come from the traefik-proxmox-provider plugin, which reads Traefik labels out of each Proxmox guest’s description field — not from the guest name, and not from container labels. A guest with no labels in its description gets no router, however it is named.
That is exactly what happened here: CT 270 was created with the stock community-script
description, and dashy.toolsera.lan returned 404 while Dashy itself was healthy on
port 4000. The fix was appending this block to the guest description, mirroring what
glance (CT 252) and pulse (CT 244) already carry:
traefik.enable=truetraefik.http.routers.dashy.entrypoints=httpstraefik.http.routers.dashy.rule=Host(`dashy.toolsera.fr`)traefik.http.routers.dashy.tls=truetraefik.http.routers.dashy.middlewares=secure-chain-auth@filetraefik.http.services.dashy.loadbalancer.server.port=4000
traefik.enable=truetraefik.http.routers.dashy-lan.rule=Host(`dashy.toolsera.lan`)traefik.http.routers.dashy-lan.entrypoints=httpstraefik.http.routers.dashy-lan.tls=truetraefik.http.routers.dashy-lan.tls.certresolver=stepcatraefik.http.routers.dashy-lan.middlewares=secure-chain-no-auth@filetraefik.http.services.dashy-lan.loadbalancer.server.port=4000Applied with pct set, preserving the existing description:
pct set 270 --description "$(cat /root/pct-backups/dashy-desc.txt)"The plugin polls every 30 seconds, so the route appears within about half a minute. No Traefik restart is needed — this is provider state, not static config.
The same gap hit VM 217 docker-apps-cifs (192.168.1.180). Its description
carried label blocks for forgejo and rustfs only, so sportarr and cleanuparr ran
healthy on :1867 and :11011 with no hostname pointing at them. Fixed the same way —
two more blocks appended to that VM’s description. A service added to
../../docker-compose-apps/docker-apps-cifs/docker-compose.yml
needs a matching block there; the compose file alone routes nothing.
arr-dashboard (:49153) and neutarr (:9705) moved onto that VM from the NAS and
carry blocks there too. Both use plain loadbalancer.server.port — the guest is the
backend now, so the server.url override the NAS services need would be wrong. Moving a
service between hosts means editing its block, not just the compose file: a stale
server.url keeps pointing at the old host and the route silently 502s.
boxarr (:8888), crosswatch (:8787) and gamevault (:8080) carry blocks there
too, appended after neutarr and before the UGREEN NAS paragraph — everything below
that line uses server.url against 192.168.1.29, so a port-based block filed under it
reads as if it belonged to the NAS.
The description is a single API field, so any edit rewrites it wholesale: keep a copy of
the current value first. It applies live, and the plugin picks the routers up within its
30-second poll. Editing it over the API is covered in
../../.claude/host-access.md.
The zone wildcards to Traefik, so a new *.toolsera.lan name needs no DNS work — it
resolves to 192.168.1.14 the moment it is invented. That cuts both ways: an
unrouted name resolves and 404s rather than failing to resolve, so getent hosts is not
evidence a route exists. Check Traefik’s router table instead.
hl- slugs resolve against homarr-labs/dashboard-icons, trying svg/{icon}.svg first
and falling back to png/{icon}.png — so a slug that exists only as a PNG (as
cleanuparr does) still works. Of the apps added with the NAS and CIFS routes, only
sportarr, qui, gluetun, qbittorrent and cleanuparr exist upstream; qbitwebui,
anibridge, arr-dashboard and rustatio are in neither directory and use mdi-
icons rather than rendering broken. The three added with this batch all resolve:
crosswatch has both formats, boxarr and gamevault are PNG-only and rely on that
fallback.
Of the 2026-08-23 batch, komga and autobrr have both formats and bookorbit is
PNG-only — the same fallback again. comicarr is in neither directory, so it uses
mdi-book-search.
Status checks
Section titled “Status checks”statusCheck is on with a 300-second interval. Checks run server-side in Dashy’s Node
process, not in the browser, so they succeed or fail on what CT 270 trusts.
Every .toolsera.lan tile initially showed unable to get issuer cert locally. Two
things were wrong, and only one of them was obvious:
statusCheckAllowInsecureis a per-item option. It had been set underappConfig, where it is silently ignored. It was never going to suppress anything.- Node ignores the system trust store. It ships its own bundled CA list, so the Smallstep root installed and active in this guest was simply never consulted.
The fix is systemd/ca-certs.conf — a unit drop-in setting
NODE_EXTRA_CA_CERTS to the system bundle, which carries the public roots and the
internal CA together. That keeps verification on rather than disabling it across every
tile. NODE_EXTRA_CA_CERTS is read once at process start, so it has to live in the
unit environment.
Checked afterwards by fetching the original 54 URLs from inside the guest with Node: 53
pass, and the one that does not is Proxmox, reached by IP with a certificate that
does not list that IP as a SAN. It reports ERR_TLS_CERT_ALTNAME_INVALID — a name
mismatch, not an untrusted issuer — so it carries the per-item
statusCheckAllowInsecure flag, the only tile that does.
The nine cluster tiles added later were verified from a workstation rather than from CT 270. That glance happened on 2026-08-23, sweeping all 80 URLs from inside the guest:
prometheus and alertmanager verify fine, exactly as expected — they are
.toolsera.lan names issued by the same Step CA, so the drop-in above covers them.
traefik-prod-talos.toolsera.lan does not. It resolves to 192.168.1.201 — the
cluster, not the fleet Traefik — and the cluster issues its own certificates, so the
handshake aborts with unknown CA while curl -k on the same URL returns 200. That is
an untrusted issuer, not the SAN mismatch the Proxmox tile has, and it now carries the
second per-item statusCheckAllowInsecure on the board. The real fix is adding the
cluster root to the guest bundle alongside the Smallstep one.
A check never reaches the service on a .toolsera.fr route. Fleet routes are answered by
Authelia, cluster routes by Cloudflare Access — both return a redirect to their
own login, so a green tile there means the auth layer is up, not the backend.