Homepage: Building a Single-Pane Dashboard for 14 K8s Services

A ConfigMap-driven dashboard that shows every service in the homelab with live widgets, organized by mental model rather than technology. No database, no state, pure GitOps.

Homepage: Building a Single-Pane Dashboard for 14 K8s Services

When you self-host 14 services, you need a single place to find them. Not a bookmark folder: those get stale. Not a text file: those get lost. A live dashboard that shows every service, its status, and a direct link. That is what Homepage does.

What Homepage Is

Homepage (gethomepage.dev) is a static dashboard application that reads its configuration from YAML files. It renders a clean, responsive page with service cards organized into groups. Each card shows the service name, a link, an icon, and optionally a status indicator or widget with live data.

It has no database. No user accounts. No build step. You write a YAML file, mount it into the container, and it renders a dashboard. Configuration changes take effect on the next page load.

The Kubernetes Deployment

Homepage runs as a single container with its configuration mounted from a ConfigMap:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: homepage
  namespace: homepage
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: homepage
        image: ghcr.io/gethomepage/homepage:v0.9.13
        ports:
        - containerPort: 3000
        volumeMounts:
        - name: config
          mountPath: /app/config
      volumes:
      - name: config
        configMap:
          name: homepage-config

The entire dashboard is defined in a ConfigMap. No PVC needed: there is no state to persist. If the pod restarts, it reads the same ConfigMap and renders the same dashboard. This makes it one of the most operationally simple workloads in the cluster.

The Configuration

Homepage uses several YAML files for configuration. The main one is services.yaml, which defines the service groups and cards:

- Infrastructure:
    - Traefik:
        icon: traefik.png
        href: https://traefik.naviauxlab.com
        description: Reverse proxy and ingress controller
        widget:
          type: traefik
          url: http://traefik.traefik.svc.cluster.local:9000

    - Longhorn:
        icon: longhorn.png
        href: https://longhorn.naviauxlab.com
        description: Distributed block storage

    - Uptime Kuma:
        icon: uptime-kuma.png
        href: https://status.naviauxlab.com
        description: Service monitoring
        widget:
          type: uptimekuma
          url: http://uptime-kuma.monitoring.svc.cluster.local:3001
          slug: services

- Applications:
    - Life Hub:
        icon: mdi-heart-pulse
        href: https://life-hub.naviauxlab.com
        description: Personal data warehouse

    - Navidrome:
        icon: navidrome.png
        href: https://navidrome.naviauxlab.com
        description: Music streaming

    - Miniflux:
        icon: miniflux.png
        href: https://miniflux.naviauxlab.com
        description: RSS reader

    - Ghost:
        icon: ghost.png
        href: https://steven.naviauxlab.com
        description: Blog

- Media:
    - Audiobookshelf:
        icon: audiobookshelf.png
        href: https://audiobookshelf.naviauxlab.com
        description: Audiobook library

    - Mealie:
        icon: mealie.png
        href: https://mealie.naviauxlab.com
        description: Recipe manager

- Security:
    - Vaultwarden:
        icon: bitwarden.png
        href: https://vault.naviauxlab.com
        description: Password manager

    - Authentik:
        icon: authentik.png
        href: https://auth.naviauxlab.com
        description: Identity provider

Each service gets an icon (either a built-in icon from the Homepage icon set, a Material Design icon via mdi- prefix, or a custom image), a link to the external URL, and a description.

Widgets

The real value of Homepage over a static bookmark page is widgets. Widgets query service APIs and display live data directly on the dashboard card.

The widgets I use:

  • Traefik: Shows active routers and services count
  • Uptime Kuma: Shows monitor counts by status (up/down/pending)
  • Longhorn: Shows total and available storage capacity
  • Miniflux: Shows unread article count

Widgets connect to services via cluster-internal URLs. This is important: the widget queries happen server-side (inside the Homepage container), not client-side (in the browser). So widget URLs use http://service.namespace.svc.cluster.local, which only resolves inside the cluster.

# widgets.yaml - system info widgets on the top bar
- kubernetes:
    cluster:
      show: true
      cpu: true
      memory: true
    nodes:
      show: true
- resources:
    cpu: true
    memory: true
- datetime:
    text_size: xl
    format:
      dateStyle: long
      timeStyle: short

The top bar shows Kubernetes cluster stats (CPU/memory across nodes) and the current date/time. This gives an at-a-glance health view without opening Grafana.

Why ConfigMap, Not a PVC

Some people mount Homepage's config directory on a PVC and edit files directly. I use a ConfigMap instead because:

  1. GitOps. The ConfigMap is defined in the home-ops repository. Changes go through Git, get reviewed (by me, but still), and are applied by Flux. The dashboard configuration is versioned and recoverable.
  2. No state to lose. Homepage doesn't store anything. The configuration IS the application. If the pod dies, the replacement reads the same ConfigMap and looks identical.
  3. Atomic updates. Changing the ConfigMap and restarting the pod gives a clean transition. No partial config reads, no file-lock issues.

The tradeoff is that ConfigMaps have a 1 MB size limit. For a dashboard config, this is more than enough: my full configuration is about 3 KB.

Authentication

Homepage sits behind Traefik's ForwardAuth middleware with Authentik. This means the dashboard is only accessible after SSO login. This is important because the dashboard contains direct links to every admin interface in the cluster (Longhorn, Traefik, Grafana), and those links should not be publicly accessible.

Homepage itself has no built-in authentication. It serves whatever it renders to whoever connects. ForwardAuth is the security boundary.

Service Discovery

Homepage supports automatic Kubernetes service discovery via annotations on Services. You can annotate a Service with gethomepage.dev/enabled: "true" and Homepage will auto-discover it and add a card.

I don't use this feature. The explicit YAML configuration is more predictable: I control exactly what appears, in what order, in what group, with what description. Automatic discovery is useful for clusters with many dynamically created services. For a homelab with 14 stable services, manual configuration is clearer.

The Layout

My dashboard has four groups arranged in a grid:

  • Infrastructure (top left): Traefik, Longhorn, Uptime Kuma, Grafana, Flux
  • Applications (top right): Life Hub, Navidrome, Miniflux, Ghost, Linkding
  • Media & Life (bottom left): Audiobookshelf, Mealie, Actual Budget
  • Security (bottom right): Vaultwarden, Authentik

The grouping follows a mental model: infrastructure stuff I check when debugging, apps I use daily, media and lifestyle tools, and security-critical services that I rarely open but need to find quickly when I do.

What Homepage Doesn't Do

No deep health checks. Widgets show basic stats, not full monitoring. If Navidrome is unhealthy, Uptime Kuma will tell me, not Homepage. The dashboard is for navigation, not observability.

No search. With 14 services, I don't need search. I know where everything is. For larger homelabs (50+ services), a dashboard with search or filtering would be necessary.

No mobile optimization. Homepage is responsive, but the widget layout is designed for a desktop or tablet. On a phone, it's usable but cramped. I typically access services directly on mobile rather than going through the dashboard.

Alternatives I Considered

Heimdall: More feature-rich (tabs, search, enhanced cards) but heavier and stores state in a database. Overkill for my use case.

Dashy: Very configurable with a built-in status checker and editor UI. But the configuration is JSON (not YAML), and the editor creates state that doesn't fit the GitOps model.

Organizr: Designed for media servers (Plex, Sonarr, Radarr). Wrong audience for my stack.

A static HTML page: The simplest option. I considered it. But widgets that show live Traefik stats and Uptime Kuma status are genuinely useful and worth the extra container.

Lessons

The dashboard is the entry point. New browser tab opens Homepage. Everything I need is one click away. This sounds trivial, but it eliminates the "what was the URL for Longhorn again?" friction that slows debugging during incidents.

ConfigMap over PVC for stateless apps. Any application that reads configuration and renders output (no database, no user uploads, no session state) should use a ConfigMap. It's simpler, more GitOps-friendly, and eliminates a PVC that would need backup and migration.

Widgets justify the tool. Without widgets, Homepage is just a fancy bookmark page. With widgets showing live cluster CPU, unread RSS count, and monitor status, it becomes a single-pane-of-glass dashboard. The server-side widget queries (using cluster-internal URLs) are what make it work in Kubernetes.

Group services by mental model, not by technology. "Infrastructure" and "Applications" is more useful than "Databases" and "Web Servers." The grouping should match how you think about your homelab, not how Kubernetes categorizes workloads.