Post

Homepage

Homepage

Homepage

Self-hosted dashboard for the homelab. Runs as homepage in the default namespace of the k3s cluster, fronted by Traefik, deployed and kept in sync by ArgoCD.

URLs: https://homepage.prod-k3s.iot-proxmox.net / https://www.homepage.prod-k3s.iot-proxmox.net

Stack

ItemValue
Imageghcr.io/gethomepage/homepage:v2.2.0
Namespacedefault
Replicas1, no nodeSelector (no PVC to stay pinned to)
ServiceClusterIP, port 8888 → container 3000
ConfigConfigMap homepage, mounted file-by-file via subPath
SecretsSecret/homepage-secret (~60 HOMEPAGE_VAR_* keys), via envFrom
GitOpsArgoCD Application — repo Homepage.git, branch k3s
Reloaderreloader.stakater.com/auto: "true" — pod restarts when the ConfigMap or Secret changes

Config delivery

Rather than mounting the whole ConfigMap over /app/config, each file Homepage expects is mounted individually via subPath, so Homepage’s own default files for anything not explicitly provided stay intact:

1
2
3
4
5
6
7
8
9
/app/config/
├── settings.yaml     # theme, layout, background
├── services.yaml     # every tile, grouped
├── widgets.yaml      # top-bar clock/search/cluster-stats
├── kubernetes.yaml   # mode: cluster
├── bookmarks.yaml    # empty
├── docker.yaml       # empty — Docker socket integration unused, Portainer covers this
├── custom.css        # empty
└── custom.js         # empty

All widget URLs, usernames, passwords, and API keys referenced via HOMEPAGE_VAR_* template interpolation live in homepage-secret — checked into the source repo as stringData, same risk profile as any other secret in this workspace, just unusually wide-reaching since it holds credentials for nearly every service on the dashboard.

RBAC

A dedicated ServiceAccount/ClusterRole/ClusterRoleBinding (homepage) grants cluster-scoped, read-only get/list on namespaces, pods, nodes, ingresses, ingressroutes (traefik.io), httproutes/gateways (gateway.networking.k8s.io), and metrics.k8s.io (nodes, pods). This powers the kubernetes widget (cluster/node CPU + memory cards, widgets.yaml) — it’s cluster-scoped rather than namespaced because that widget reads node metrics cluster-wide. The ingresses/ingressroutes read access also supports Homepage’s optional auto-discovery via annotations, though every tile here is defined explicitly in services.yaml rather than relying on it.

Settings

  • Theme: Dark / Slate, custom background image, blur sm, brightness 50
  • Layout: useEqualHeights: true, all groups style: row
  • Widgets: Resources → replaced by the kubernetes cluster/node widget, plus datetime (short time, XL text) and search (Google, new tab)

Service groups (services.yaml)

GroupColsTiles
Hypervisor3PVE (proxmox), PBS (proxmoxbackupserver), Cockpit
Network3Gateway (unifi), Traefik (traefik), Gatus (gatus)
Infrastructure3Portainer (portainer, kubernetes: true), TinyAuth, Filebrowser
Storage3TrueNAS (truenas, pools enabled), Longhorn, Pi-NAS Backup
Observability3Grafana (grafana), pgAdmin, DBeaver
Media2Jellyfin (jellyfin), Immich (immich)
Media-converter2Handbrake, Media Index (customapi/api/counts: files/backlog/jobs)
Arr-config3qBittorrent (qbittorrent), Prowlarr (prowlarr), Profilarr
Arr2Radarr (radarr), Sonarr (sonarr)
Home Automation2Home Assistant (link only), Frigate (frigate, recent events)
Development3Code-Server, GitHub, Gitea (gitea)
Productivity2Invoice Ninja, Drawio
AI3n8n, Open WebUI, Ollama
DataTesting4ActiveMQ, Kafka, S3 (RustFS), FTP (SFTPGo)

The Media/Media-converter/Arr/AI/DataTesting groups reflect apps added since the original Docker Compose deployment — the tile list is kept in sync with whatever is actually running on the cluster, not carried over as a static snapshot.

GitOps

argocd/apps/homepage.yaml defines the ArgoCD Application: it tracks the k3s branch of Homepage.git (the main branch still holds the legacy Docker Compose deployment for reference, and is Renovate’s default branch), applies every *.yaml in the repo root against the pre-existing default namespace, and syncs with prune: true + selfHeal: true — any manual kubectl change is reverted back to what’s committed. In practice, changes ship by committing to k3s, not by running kubectl apply by hand.

Renovate reads its config from main (its default branch) but is configured with baseBranchPatterns: ["main", "k3s"] so it also opens PRs against k3s, with kubernetes.fileMatch enabled so it tracks the Homepage image tag in deployment.yaml, and automerges minor/patch/pin/digest bumps.

This post is licensed under CC BY 4.0 by the author.