One of the challenges of running a homelab with dozens of services is keeping track of what’s running and where. I recently deployed Homepage - a modern, fully static dashboard that automatically discovers services from Kubernetes ingresses.
Why Homepage?
I evaluated several dashboard options including Homarr and Heimdall. Homepage stood out for a few reasons:
- Kubernetes-native service discovery - no manual configuration needed
- Real-time pod status - shows if services are actually running
- Clean, modern UI - dark theme, customizable layout
- Lightweight - just a static site with no database
The Setup
Homepage runs as a simple deployment in Kubernetes with a ServiceAccount that has read access to the cluster. The magic happens through ingress annotations.
Base Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: homepage
spec:
replicas: 1
template:
spec:
serviceAccountName: homepage
initContainers:
- name: copy-config
image: busybox:1.36
command: ['sh', '-c', 'cp /config-source/* /config/']
volumeMounts:
- name: config-source
mountPath: /config-source
- name: config
mountPath: /config
containers:
- name: homepage
image: ghcr.io/gethomepage/homepage:v0.10.9
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config-source
configMap:
name: homepage-config
- name: config
emptyDir: {}
The init container pattern is important here - Homepage needs a writable config directory to create log files, but ConfigMaps are read-only. We copy the config to an emptyDir volume at startup.
Service Discovery via Annotations
The real power comes from ingress annotations. For each service you want on the dashboard, add these annotations:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frigate
annotations:
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Frigate"
gethomepage.dev/group: "Home Automation"
gethomepage.dev/icon: "frigate.png"
gethomepage.dev/pod-selector: "app=frigate"
The pod-selector annotation is crucial for status monitoring. Homepage uses this to find the actual pods and show whether they’re running. Without it, you’ll see “not found” errors.
ConfigMap for Layout
The ConfigMap controls the dashboard layout and widgets:
apiVersion: v1
kind: ConfigMap
metadata:
name: homepage-config
data:
settings.yaml: |
title: Homelab
theme: dark
color: slate
headerStyle: clean
layout:
Home Automation:
style: row
columns: 4
Media:
style: row
columns: 4
Infrastructure:
style: row
columns: 4
widgets.yaml: |
- kubernetes:
cluster:
show: true
cpu: true
memory: true
showLabel: true
label: "cluster"
nodes:
show: true
cpu: true
memory: true
kubernetes.yaml: |
mode: cluster
services.yaml: |
[]
docker.yaml: |
Note that services.yaml and docker.yaml must be valid YAML (even if empty). Setting services.yaml: "[]" prevents JavaScript errors from empty files.
Gotchas I Encountered
1. Empty Config Files Cause JS Errors
If services.yaml or docker.yaml are empty or contain only comments, Homepage throws a classList null reference error. Always set them to valid YAML.
2. Pod Selector Mismatches
Homepage defaults to looking for pods with app.kubernetes.io/name=<ingress-name>. Most of my deployments use simpler labels like app=frigate. The gethomepage.dev/pod-selector annotation fixes this.
3. Icon Names
Icons come from the Dashboard Icons project. Some names aren’t obvious:
- ArgoCD:
argo-cd.png(notargocd.png) - Z-Wave JS:
mdi-z-wave(Material Design Icons format)
Result
Now every time I add a new service with the proper annotations, it automatically appears on my dashboard with real-time status. No more manually updating bookmark pages or forgetting what port something runs on.
The dashboard shows CPU/memory usage for the cluster and nodes, groups services logically, and immediately tells me if something is down. It’s become my starting point for accessing everything in the homelab.