Skip to main content

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 LibraryHomer
PurposeSSO portal β€” only apps wired to AuthentikNavigation dashboard β€” every service on the platform
Entry criteriaMust have an Authentik Application configured (OIDC or forward-auth)Manual ConfigMap entry
AuthClicking launches an SSO login flowDumb 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.

HomerBackstage Catalog
The question it answers"I need to get to X right now""What IS X, who owns it, how does it work?"
Auth requiredNo β€” loads instantly, no loginYes β€” Authentik OIDC login
AudienceAnyone: ops, management demo, incident responseEngineers: developers, platform team
What it showsBig tiles, one click to any URLOwner, GitHub repo, ArgoCD sync state, pod status, Grafana links, API specs
ResilienceWorks even if Authentik is down (static HTML)Requires Authentik + PostgreSQL + k8s
SpeedInstant (static, no API calls)Loads dynamically from catalog DB
Entry methodManual ConfigMap tilecatalog-info.yaml committed to each repo

Decision table β€” which one to open:

ScenarioTool
Open Grafana quickly during an incidentHomer
Find which team owns the rag-ingest serviceBackstage
Demo the platform to a manager (no SSO setup)Homer
Onboard a new engineer who needs to understand what runs hereBackstage
Authentik is down, you need ArgoCD immediatelyHomer
Find the GitHub repo for a given serviceBackstage
Check if a deployment is healthy across all environmentsBackstage (Kubernetes plugin)
Share a quick link to the Chat interfaceHomer

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 the config-checksum annotation
  • Backstage: add a catalog-info.yaml to the service repo and register it as a catalog location in backstage-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:

  1. Edit manifests/homer/01-configmap.yaml β€” add/remove/modify the services block
  2. Bump manifests/homer/02-deployment.yaml β€” change the config-checksum annotation 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:

SectionTilesNotes
InfrastructureMAAS, set-hog, fast-skunk, fast-heron, star-kittenAll 4 worker nodes + control plane
ObservabilityGrafana, Prometheus, Alertmanager, PolarisPrometheus/Alertmanager/Polaris: internal tag (Tailscale only)
IdentityAuthentik, VaultwardenBoth public via Cloudflare Tunnel
DevOpsArgoCD, GitHub, Harbor, MinIOMinIO: internal (controller Tailscale IP)
Appsplatform-demo, Backstage, Chat, LiteLLM Admin, Langfuse, NATS, Nextcloud, Vault, ktayl-solution, podinfo, whoamipodinfo/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.io tiles route through the cluster NGINX Ingress. Mac access works because Tailscale's subnet-route advertisement makes 10.0.0.0/24 reachable.
  • 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​

ToolDocker imageFeatures
Homerb4bz/homerLightweight, YAML config, no DB
Dashylissy93/dashyRich UI, status checks, widgets
Heimdalllinuxserver/heimdallApp launcher, search bar
Flamepawelmalak/flameBookmarks + 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