Homepage GitOps Configuration¶
Date: 2026-07-19 Hostname: docker-host (LXC 100) IP address: 192.168.0.110
Homepage is the LAN service dashboard at homepage.lan, served by the homepage Docker stack on port 3002. The stack is managed by Komodo and now uses the homelab repo as the source of truth for the Homepage application config, not the old live YAML files under /srv/docker-data/homepage.
Source of truth¶
The versioned config lives here:
Komodo deploys the stack from its checkout on LXC 100:
The running container mounts:
/etc/komodo/repos/github/compose/proxmox-lxc-100/homepage/config -> /app/config
/srv/docker-data/homepage/logs -> /app/config/logs
/srv/docker-data/homepage/images -> /app/public/images
The old live files such as /srv/docker-data/homepage/services.yaml are no longer the source of truth after the GitOps deploy. Edit the repo copy instead.
Komodo stack settings¶
The existing homepage stack is registered in Komodo with these relevant settings:
| Setting | Value |
|---|---|
| Branch | main |
| Run directory | compose/proxmox-lxc-100/homepage/ |
| Compose file | docker-compose.yml |
| Environment file path | stack.env |
| Stack environment | Komodo writes this during deploy |
The compose file reads the generated environment file:
Secret handling¶
Do not commit .env, stack.env, API keys, token values or passwords. Homepage YAML files must reference secrets through placeholders:
The actual HOMEPAGE_VAR_* values live in Komodo Stack Environment and are written to stack.env during deploy. No extra environment variable is required for the GitOps config mount.
Change workflow¶
To change dashboard links, widgets, layout or service monitors:
- Edit files under
compose/proxmox-lxc-100/homepage/config/. - Commit and push the repo.
- In Komodo, run
Pulland thenDeployon thehomepagestack. curl http://192.168.0.110:3002/api/revalidate- the/route is static, so a config change is not guaranteed to reach the rendered page on its own.- Verify the container is healthy and
homepage.lanloads.
Example verification from LXC 100:
2026-07-19 migration¶
The live config was copied from /srv/docker-data/homepage, sanitized, committed to git, pushed, then deployed through Komodo. The topology.lan dashboard card is now in the versioned services.yaml and no longer exists only as live config drift.
Validation performed after deploy:
- Komodo
PullStackcompleted successfully - Komodo
DeployStackcompleted successfully homepagecontainer restarted and reported healthy/app/configpoints to the Komodo repo checkout- Homepage API returned the
Topologycard withhttp://topology.lanand monitorhttp://192.168.0.110:3009
2026-08-28: the K3s cluster's four web UIs¶
Argo CD, Longhorn, Grafana and Forgejo were added as their own K3s group -
that is every GUI the cluster has. They went in as services, not bookmarks:
Quick Links is for third-party sites and carries no siteMonitor, and for a
cluster that is only reachable over Tailscale the status dot is the point. If
the remote site loses power, that is what you want the dashboard to tell you.
Two things had to be measured, because neither is visible from the config.
argocd.png does not exist¶
The icon is argo-cd.png. Both argocd.png and argocd return 404 from
dashboard-icons and from the selfhst set. Homepage does not report a missing
icon - it renders an empty tile and logs nothing, so the name has to be checked
rather than guessed:
curl -s -o /dev/null -w '%{http_code}\n' \
https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/argo-cd.png
A Tailscale Ingress needs the hostname, not the IP¶
LXC 100 cannot resolve *.tailc6abe2.ts.net: tailscaled runs there with
accept-dns=false on purpose, so there is no MagicDNS. The obvious workaround -
point siteMonitor at the raw tailnet IP, the way the Code Server card does -
does not work here, and it fails in a way that reads as an outage:
tailscale ping 100.87.160.51 pong
TCP connect to 100.87.160.51:443 succeeds
curl https://100.87.160.51/ 000
curl --resolve argocd...:443:100.87.160.51 https://argocd... 200
The Tailscale Ingress selects the backend by SNI. A request addressed to the bare IP completes the TCP handshake and then dies in TLS, so the card would sit on a red dot with a perfectly healthy service behind it.
The fix is four extra_hosts entries on the homepage service in
docker-compose.yml. That resolves the names inside this one container and does
not touch host DNS, which is what makes it safe on a machine running 24 stacks:
extra_hosts:
- "argocd.tailc6abe2.ts.net:100.87.160.51"
- "longhorn.tailc6abe2.ts.net:100.127.135.123"
- "grafana.tailc6abe2.ts.net:100.73.118.70"
- "forgejo.tailc6abe2.ts.net:100.99.251.110"
These IPs are not stable forever. Each Ingress consumes its own tailnet
device, created by the Tailscale Kubernetes operator; if an Ingress is deleted
and recreated, its address changes and the card goes red while the service is
fine. tailscale status | grep -E 'argocd|longhorn|grafana|forgejo' is the
check.
extra_hosts also only takes effect on a container recreate, so this needs
Komodo Deploy, not Restart - and therefore the /api/revalidate call from
the section above.
Validation performed after deploy:
- container recreated,
HostConfig.ExtraHostscarries all four entries docker exec homepage cat /etc/hostsshows the four names- from inside the container: argocd 200, longhorn 200, grafana 302, forgejo 200
/api/revalidatereturned{"revalidated":true}- screenshot confirms four cards with icons and green dots
The href stays the tailnet name, so the cards only open from a device on the
tailnet. That is deliberate: these UIs have no LAN path, and the Longhorn one has
no authentication at all.
2026-09-07: one more card, and the three things it cost¶
The appointment book described in Pedicure Practice App went onto the dashboard. One card, six lines of YAML, and three separate decisions that a diff review would not have caught.
A seventh tile in a six-tile row is not free¶
Every group in settings.yaml sets columns to its own item count, so no row
is ever partially filled. The obvious edit was Utilities: columns: 6 to 7.
Rendered, that narrowed every column in the row from about 230 px to 200 px,
which was enough to wrap the Code Server and Kan descriptions onto a second
line - and with useEqualHeights: true the whole row grew by a line. Two cards
nobody touched looked different.
The card went into the nested Utilities Links strip instead, 3 columns to 4.
There the tile is about 480 px wide, every description stays on one line, and
the main row is untouched. A before/after pixel diff of the full 1920x2600 page
showed differences in exactly four bands: the header clock, three widget number
lines carrying live data, and the strip itself.
siteMonitor wants a health endpoint, not /¶
The app's / answers 303 to /login. /health answers 200, and it is
what the container's own healthcheck already calls, so that is what the dot
should follow. Two entries in this file were already built that way - the
Hermes and Minions cards on LXC 113 both point at /health and /api/health.
An http link to an app with Secure cookies is a broken link¶
The tempting href was http://192.168.0.110:3010: same LAN, no reverse proxy,
no round trip through the VPS. But the stack sets PEDIKUR_HTTPS_ONLY=1, so the
session cookie carries the Secure attribute, the browser refuses to store it
over plain http, and the login form silently reloads instead of failing. There
is no error message anywhere - not in the app log, not in the browser console.
So href is the public https route (a placeholder here, as for every app that
holds anything sensitive) and siteMonitor stays local:
- Pedikur:
icon: mdi-foot-print
href: https://your-pedikur.yourdomain.com
description: Appointment book
siteMonitor: http://192.168.0.110:3010/health
statusStyle: dot
The dot then reports the container rather than the reverse proxy in front of it, which is the more useful failure signal: the tunnel being down and the app being down are different problems.
Verify a layout change before it touches the live dashboard¶
A second homepage container, on a spare port, with the candidate config mounted read-only, costs nothing and renders the real thing:
rsync -a --exclude logs config/ root@192.168.0.110:/tmp/hp-preview/config/
docker run -d --name hp-preview \
-e HOMEPAGE_ALLOWED_HOSTS=192.168.0.110:3099 -e PUID=0 -e PGID=0 \
-v /tmp/hp-preview/config:/app/config -p 3099:3000 \
ghcr.io/gethomepage/homepage:latest
curl http://192.168.0.110:3099/api/revalidate # not optional, see below
Widgets show API errors because the preview has no stack.env, and that is
fine - the layout is what is being checked.
The revalidate call is load-bearing on a fresh container too. The first
render of the preview ignored settings.yaml completely: no background, no
headerStyle, no layout block, groups stacked as columns instead of rows,
and the bookmarks strip at the bottom of the page instead of the top. It looked
like a version incompatibility. Both containers were running the same image
(gethomepage 2.2.0, same digest), and /api/revalidate fixed it in one call.
The / route is generated by getStaticProps, so the page a new container
first serves is not necessarily built from the config that is mounted into it.
Judging that first screenshot would have produced a redesign chasing a fault that did not exist.
Credential hygiene¶
The Komodo repo checkout on LXC 100 currently uses a tokenized HTTPS Git remote. Treat that token as a secret and do not copy it into docs or logs. Long term, migrate the checkout to a GitHub deploy key or SSH remote, then rotate the old personal access token.