Homer β Self-Hosted Web Dashboard
Homer is a lightweight static dashboard that runs as a pod in your k3s cluster. It gives you a single browser page with tiles linking to every service in your infrastructure β accessible via Tailscale or Cloudflare Tunnel.
What It Looks Likeβ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Mini Cloud Platform β
β Private Bare-Metal Infrastructure β
ββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬ββββββββββ€
β MAAS β Grafana β ArgoCD β GitLab β
β Bare-metal β Monitoring β GitOps β CI/CD β
β provisioner β dashboards β sync β β
ββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββ΄ββββββββββ€
β Kubernetes β
β set-hog (CP) Β· fast-skunk Β· fast-heron β
β β β β All nodes Ready β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Homer vs Authentik Library β What's the Difference?β
Both provide a list of platform services, but they serve completely different purposes:
| Authentik Library | Homer | |
|---|---|---|
| Purpose | SSO portal β only apps wired to Authentik | Navigation dashboard β every service on the platform |
| Entry criteria | Must have an Authentik Application configured (OIDC or forward-auth) | Manual ConfigMap entry |
| Auth | Clicking launches an SSO login flow | Dumb link β no auth, just navigates |
| Audience | "Log into app X via Authentik" | "Where is app X?" |
Authentik shows what it protects. Homer shows what exists.
This means:
- Services accessible only via Tailscale (MAAS, Prometheus, Alertmanager, MinIO) appear in Homer but not in Authentik β they don't need SSO because they're network-isolated.
- Authentik itself appears in Homer, but not in its own library (a portal can't protect itself).
- A new service must be added to both independently: one Authentik Application for SSO, one Homer ConfigMap entry for navigation.
:::tip Keep them in sync When you deploy a new service, add it to Homer's ConfigMap at the same time. It's easy to forget β the platform works fine without it, but Homer becomes stale. :::
Homer vs Backstage Catalog β What's the Difference?β
Both show a list of platform services. The question they answer is completely different.
| Homer | Backstage Catalog | |
|---|---|---|
| The question it answers | "I need to get to X right now" | "What IS X, who owns it, how does it work?" |
| Auth required | No β loads instantly, no login | Yes β Authentik OIDC login |
| Audience | Anyone: ops, management demo, incident response | Engineers: developers, platform team |
| What it shows | Big tiles, one click to any URL | Owner, GitHub repo, ArgoCD sync state, pod status, Grafana links, API specs |
| Resilience | Works even if Authentik is down (static HTML) | Requires Authentik + PostgreSQL + k8s |
| Speed | Instant (static, no API calls) | Loads dynamically from catalog DB |
| Entry method | Manual ConfigMap tile | catalog-info.yaml committed to each repo |
Decision table β which one to open:
| Scenario | Tool |
|---|---|
| Open Grafana quickly during an incident | Homer |
Find which team owns the rag-ingest service | Backstage |
| Demo the platform to a manager (no SSO setup) | Homer |
| Onboard a new engineer who needs to understand what runs here | Backstage |
| Authentik is down, you need ArgoCD immediately | Homer |
| Find the GitHub repo for a given service | Backstage |
| Check if a deployment is healthy across all environments | Backstage (Kubernetes plugin) |
| Share a quick link to the Chat interface | Homer |
Analogy: Homer is your browser bookmarks bar. Backstage is your internal Wikipedia + ops dashboard rolled into one, per service.
Homer is not redundant β it is intentionally simpler and more resilient. Backstage going down does not affect Homer at all. Homer losing a tile does not affect Backstage. They serve different audiences and different cognitive modes (navigate fast vs understand deeply).
:::info Both must be updated when a new service is deployed
- Homer: add a tile to
manifests/homer/01-configmap.yaml+ bump theconfig-checksumannotation - Backstage: add a
catalog-info.yamlto the service repo and register it as a catalog location inbackstage-values.yaml
These are independent. A service can be in Homer only (no catalog metadata), in Backstage only (no tile), or in both. Aim for both. :::
Deploy Homer in k3sβ
Create the namespace and configβ
kubectl create namespace homer
Create the Homer config file (you'll mount it as a ConfigMap):
cat <<'EOF' > homer-config.yml
title: "Mini Cloud Platform"
subtitle: "Private Bare-Metal Infrastructure"
logo: "logo.png"
header: true
footer: false
colors:
light:
highlight-primary: "#3367d6"
highlight-secondary: "#4285f4"
highlight-hover: "#5a95f5"
background: "#f2f4f8"
card-background: "#ffffff"
text: "#363636"
text-header: "#ffffff"
text-title: "#303030"
text-subtitle: "#424242"
card-shadow: rgba(0, 0, 0, 0.1)
link: "#3273dc"
link-hover: "#363636"
dark:
highlight-primary: "#3367d6"
highlight-secondary: "#4285f4"
highlight-hover: "#5a95f5"
background: "#131313"
card-background: "#2b2b2b"
text: "#eaeaea"
text-header: "#ffffff"
text-title: "#fafafa"
text-subtitle: "#f5f5f5"
card-shadow: rgba(0, 0, 0, 0.4)
link: "#3273dc"
link-hover: "#ffdd57"
services:
- name: "Infrastructure"
icon: "fas fa-server"
items:
- name: "MAAS"
logo: "https://assets.ubuntu.com/v1/0b5f1b3e-maas-logo.svg"
subtitle: "Bare-metal provisioning"
tag: "infra"
url: "http://10.0.0.1:5240/MAAS"
target: "_blank"
- name: "set-hog"
icon: "fas fa-microchip"
subtitle: "Control plane β 10.0.0.2"
tag: "k8s"
url: "http://10.0.0.1:5240/MAAS/r/machines"
target: "_blank"
- name: "fast-skunk"
icon: "fas fa-microchip"
subtitle: "Worker β 10.0.0.4"
tag: "k8s"
url: "http://10.0.0.1:5240/MAAS/r/machines"
target: "_blank"
- name: "fast-heron"
icon: "fas fa-microchip"
subtitle: "Worker β 10.0.0.7"
tag: "k8s"
url: "http://10.0.0.1:5240/MAAS/r/machines"
target: "_blank"
- name: "Observability"
icon: "fas fa-chart-line"
items:
- name: "Grafana"
logo: "https://grafana.com/static/assets/img/fav32.png"
subtitle: "Metrics & dashboards"
tag: "monitoring"
url: "http://10.0.0.2:3000"
target: "_blank"
- name: "Prometheus"
icon: "fas fa-fire"
subtitle: "Metrics storage"
tag: "monitoring"
url: "http://10.0.0.2:9090"
target: "_blank"
- name: "DevOps"
icon: "fas fa-code-branch"
items:
- name: "ArgoCD"
icon: "fas fa-sync"
subtitle: "GitOps β continuous deployment"
tag: "gitops"
url: "http://10.0.0.2:8080"
target: "_blank"
- name: "GitLab / Gitea"
icon: "fas fa-code"
subtitle: "Git platform & CI/CD"
tag: "cicd"
url: "http://10.0.0.2:80"
target: "_blank"
EOF
Create Kubernetes manifestsβ
cat <<'EOF' > homer-deployment.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: homer-config
namespace: homer
data:
config.yml: |
# paste the contents of homer-config.yml here
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: homer
namespace: homer
spec:
replicas: 1
selector:
matchLabels:
app: homer
template:
metadata:
labels:
app: homer
spec:
containers:
- name: homer
# Pinned 2026-06-15 β see "Image tag pinning" section
image: b4bz/homer:v26.4.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
volumeMounts:
- name: config
mountPath: /www/assets/config.yml
subPath: config.yml
volumes:
- name: config
configMap:
name: homer-config
---
apiVersion: v1
kind: Service
metadata:
name: homer
namespace: homer
spec:
type: NodePort
selector:
app: homer
ports:
- port: 8080
targetPort: 8080
nodePort: 30902
EOF
kubectl apply -f homer-deployment.yaml
Verifyβ
kubectl get pods -n homer
kubectl get svc -n homer
Homer is now running at:
http://10.0.0.2:30902 β from inside the network
http://100.x.x.x:30902 β via Tailscale
https://dashboard.yourdomain.com β via Cloudflare Tunnel
Connect to Cloudflare Tunnelβ
Update your ~/.cloudflared/config.yml on the MAAS controller:
- hostname: dashboard.yourdomain.com
service: http://10.0.0.2:30902
Then reload:
sudo systemctl restart cloudflared
Connect to Tailscaleβ
No extra config needed. If the MAAS controller has the 10.0.0.0/24 route advertised via Tailscale, Homer is automatically reachable at:
http://10.0.0.2:30902
from any Tailscale-connected machine.
Customize Homerβ
Original method (pre-Phase 12, ad-hoc)β
Edit homer-config.yml and update the ConfigMap:
kubectl create configmap homer-config \
--from-file=config.yml=homer-config.yml \
-n homer \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/homer -n homer
This works for one-off local development. Do NOT use it in production β ArgoCD's selfHeal (Phase 12) reverts any kubectl-applied changes within ~3 minutes.
Production method (Phase 12 onwards β GitOps-managed)β
Homer is one of the workloads managed by ArgoCD's app-of-apps pattern. Source-of-truth lives in andrelair-platform/minicloud-gitops at manifests/homer/. Two edits are needed for every tile change:
- Edit
manifests/homer/01-configmap.yamlβ add/remove/modify theservicesblock - Bump
manifests/homer/02-deployment.yamlβ change theconfig-checksumannotation value (e.g.v10-minio-tileβv11-github-tile)
The annotation bump is essential. Homer mounts the ConfigMap with subPath, which Kubernetes does NOT auto-propagate when the ConfigMap changes. Without the annotation change, the pod keeps serving the stale tile list forever even after the ConfigMap update succeeds. Annotation change β pod-template hash changes β rolling restart β new ConfigMap picked up. (Production teams automate this with Reloader or kustomize configMapGenerator. We do it by hand for the learning value.)
After both file edits:
cd minicloud-gitops
git add manifests/homer/
git commit -m "feat(homer): <what you changed>"
git push origin main
# ArgoCD reconciles within ~3 min:
kubectl get app homer -n argocd -w
# Watch for: Synced β OutOfSync β Synced β Progressing β Healthy
Current tile inventory (config-checksum v17, 2026-07-10)β
The dashboard is organized into five sections:
| Section | Tiles | Notes |
|---|---|---|
| Infrastructure | MAAS, set-hog, fast-skunk, fast-heron, star-kitten | All 4 worker nodes + control plane |
| Observability | Grafana, Prometheus, Alertmanager, Polaris | Prometheus/Alertmanager/Polaris: internal tag (Tailscale only) |
| Identity | Authentik, Vaultwarden | Both public via Cloudflare Tunnel |
| DevOps | ArgoCD, GitHub, Harbor, MinIO | MinIO: internal (controller Tailscale IP) |
| Apps | platform-demo, Backstage, Chat, LiteLLM Admin, Langfuse, NATS, Nextcloud, Vault, ktayl-solution, podinfo, whoami | podinfo/whoami: internal |
Notes on tile design decisions:
- star-kitten added as 4th worker node (10.0.0.8) β same MAAS machine page URL as the others.
- Polaris (
polaris.10.0.0.200.nip.io) is internal-only β the dashboard blocks during its ~60s audit cycle; access via Tailscale between cycles. - MinIO tile points at
http://100.88.123.8:9001(controller's Tailscale IP) β MinIO runs as a Docker container on the controller, not in the k3s cluster. - All
*.10.0.0.200.nip.iotiles route through the cluster NGINX Ingress. Mac access works because Tailscale's subnet-route advertisement makes10.0.0.0/24reachable. - Prometheus and Alertmanager are tagged
internalβ protected by Authentik forward-auth on their nip.io Ingress. Direct port-forward not needed. - Chat subtitle reflects the full AI Gateway: 7 Ollama models, RAG (bge-m3 + pgvector), SearXNG web search, LiteLLM multi-provider routing.
Alternative Dashboardsβ
| Tool | Docker image | Features |
|---|---|---|
| Homer | b4bz/homer | Lightweight, YAML config, no DB |
| Dashy | lissy93/dashy | Rich UI, status checks, widgets |
| Heimdall | linuxserver/heimdall | App launcher, search bar |
| Flame | pawelmalak/flame | Bookmarks + weather widgets |
Homer is recommended for simplicity β no database, purely static, minimal resources (~10MB RAM).
Image tag pinning (2026-06-15)β
Homer was originally installed with image: b4bz/homer:latest and imagePullPolicy: Always β the "pull the newest thing on every restart" pattern. During the 2026-06-15 platform-wide :latest audit (one of only two :latest references found cluster-wide), this was pinned to a specific version.
Change:
- image: b4bz/homer:latest
- imagePullPolicy: Always
+ # Pinned 2026-06-15
+ image: b4bz/homer:v26.4.2
+ imagePullPolicy: IfNotPresent
Applied via the GitOps workflow above: edit manifests/homer/02-deployment.yaml in minicloud-gitops β commit + push β ArgoCD reconciles. Force-sync with kubectl patch app homer -n argocd --type merge -p '{"operation":{"sync":{}}}' if you don't want to wait for the 3-min cycle.
Why v26.4.2: Newest published tag at pin time, immutable, low surprise risk (Homer is static HTML/JS β major version changes are infrequent and well-publicized).
Why imagePullPolicy: IfNotPresent: With a mutable :latest tag, Always was required to ensure pod restarts caught updates. With an immutable tag, Always is pure waste β every pod restart hits the registry for an image we know hasn't changed. IfNotPresent is correct: pull once per node, reuse from containerd cache.
Cluster-wide audit pattern (worth keeping in your shell history for periodic re-checks):
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: {range .spec.containers[*]}{.image}{"\n"}{end}{end}' | grep ':latest$'
Empty output = cluster is fully pinned. The full incident story (which surfaced the same :latest anti-pattern on Backstage with much more dramatic symptoms β a NotImplementedError overlay on every page load) is documented in the Phase 18 doc.
Done Whenβ
β Homer pod Running in homer namespace
β Dashboard accessible at NodePort :30902
β All service tiles open correct URLs
β Accessible via Tailscale AND/OR Cloudflare Tunnel