Homepage on Kubernetes with Kapitan
homepage, a dashboard of links, service widgets and bookmarks, with its dashboard written in the inventory and optional service discovery in the cluster, designed in kadet-hub on homepage’s image and rendered by the kadet-hub kubernetes generator 1.14.0.
- Latest version:
1.0.0, published - Source: container image
ghcr.io/gethomepage/homepage:v2.7.0@sha256:19c5c09184458a41b7b5915e71463a39f36cd33540f41bdbed008791b8182d5f, recipe designed in kadet-hub - Proof level:
deploys, deployed on kind and smoke tested by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade D 31 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 0, HIGH 12, MEDIUM 17, LOW 2, UNKNOWN 0. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:ghcr.io/gethomepage/homepage.
What it deploys
recipes.homepage(init.yml)recipes.homepage.homepage(homepage.yml):homepagerecipes.homepage.kubernetes(kubernetes.yml), an option:homepage
Required parameters
applications.homepage.allowed_hosts is the value of
HOMEPAGE_ALLOWED_HOSTS (DEC-227 in the spec): the hosts, comma-separated
and without spaces, that clients name in the Host header of their
requests, each with the port where the URL has one, such as
home.example.com behind an Ingress or homepage.homepage.svc:3000 inside
the cluster. homepage answers every other host with 400, its health check
too. localhost:3000 and 127.0.0.1:3000 are always allowed, so
kubectl port-forward to the local port 3000 works without an entry. *
turns the check off, which homepage’s documentation advises against.
Without the parameter the compile stops with Interpolation key and its
name.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.homepage
# Optional: service discovery and statistics from the cluster's API.
- recipes.homepage.kubernetes
parameters:
namespace: homepage
kapitan:
vars:
target: homepage
applications:
homepage:
allowed_hosts: home.example.com
settings:
title: Home
services:
- Media:
- Jellyfin:
href: https://jellyfin.example.com/
description: Media server
bookmarks: []
widgets: []
Fetching it
The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.
A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.0:
parameters:
kapitan:
dependencies:
- type: oci
source: ghcr.io/kadet-hub/recipes/homepage:1.0.0@sha256:6dc78d48b0212afabcec930925c4c58a9e11747eec93d2eb374901b8e2080fcb
output_path: inventory/classes/recipes/homepage
The first command fetches the recipe into inventory/classes/recipes/homepage, the second the generators and compiles:
kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch
The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe:
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/homepage@sha256:6dc78d48b0212afabcec930925c4c58a9e11747eec93d2eb374901b8e2080fcb \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.
Settings
The dashboard is four parameters, each written as YAML to its file in
/app/config (DEC-226). Each takes what homepage’s documentation shows for
its file, as inventory YAML. The recipe sets each empty, so the dashboard is
what the target writes.
| homepage file | Inventory | Default |
|---|---|---|
settings.yaml |
applications.homepage.settings |
{} |
services.yaml |
applications.homepage.services |
[] |
bookmarks.yaml |
applications.homepage.bookmarks |
[] |
widgets.yaml |
applications.homepage.widgets |
[] |
kubernetes.yaml, with the option kubernetes |
applications.homepage.kubernetes |
mode: cluster |
Kapitan’s resolver yaml writes the keys of a mapping in sorted order.
homepage orders the groups of the page by layout in settings.yaml, so
write layout as a list of one-key mappings, which homepage reads in list
order:
settings:
layout:
- Media:
style: row
- Infrastructure:
style: columnA list set in the target is appended to the list of the classes before it.
To replace a list a class sets, set it to ~ in a class included after
that class, and the new list in the target.
The ConfigMap’s name carries a hash of its content, so a changed dashboard
rolls out a new pod. Earlier ConfigMaps stay in the namespace until they
are deleted. homepage reads secrets for widgets from {{HOMEPAGE_VAR_*}}
and {{HOMEPAGE_FILE_*}} placeholders in these files. Set such variables
through components.homepage.env and components.homepage.secrets with a
ref, rather than writing a credential into the dashboard.
| Setting | Inventory |
|---|---|
| homepage version | components.homepage.image, tag and digest |
| exposure | components.homepage.service.type, or a route or Ingress of the target to the Service homepage on port 3000 |
other variables, such as LOG_LEVEL or homepage’s login with HOMEPAGE_AUTH_ENABLED |
components.homepage.env.<variable>, credentials through components.homepage.secrets |
custom.css, custom.js and other files of /app/config |
components.homepage.config_maps.config.data.<file>.value with a mount in components.homepage.volume_mounts, as homepage.yml mounts settings.yaml |
| resources | components.homepage.resources |
homepage serves the dashboard without a login, as its default (DEC-229). Anyone who reaches the Service sees the dashboard and what its widgets fetch from the services they name. homepage’s documentation asks for a reverse proxy with authentication, or its own login, in front of a public deployment.
/app/config is an emptyDir. homepage writes its log there and copies
each file it reads and the recipe does not mount, such as docker.yaml,
from the defaults in its image. The dashboard’s files are mounted over it
from the ConfigMap.
Proof
check homepage --level deploys with tests/target.yml, which includes
the option kubernetes, on kind for each Kubernetes minor CI tests, with
check 0.17.0. The image has no Kubernetes objects to compare with, so the
recipe is proven by deploying it (RCP-44). check lint accepts the
ClusterRole because it only reads and the binding because it binds that
ClusterRole (RCP-308).
tests/smoke checks that the pod passes Pod Security restricted and
starts a pod of homepage’s image, which carries node. From there, through
the Service homepage on port 3000, it
- requires 200 from
/api/healthcheckand the title oftests/target.ymlfrom/, and 400 from/with aHostoutsideapplications.homepage.allowed_hosts - finds the service and the bookmark of
tests/target.ymlin/api/servicesand/api/bookmarks - requires that the ServiceAccount
homepagemay list Ingresses and may not read Secrets, create Ingresses or delete Pods - creates an Ingress with homepage’s annotations and finds it as a service
in
/api/services - compiles
tests/target.ymlwith the service renamed, applies the result and, once the new pod rolled out, finds the new name and not the old one.
Not tested: the page in a browser, service widgets, Gateway API and Traefik discovery, Pod statistics, which need a metrics server, and homepage’s login.
Versions
1.0.0, :sha256:6dc78d48b0212afabcec930925c4c58a9e11747eec93d2eb374901b8e2080fcb
The recipe's files are in kadet-hub/recipes. All recipes