Home Assistant on Kubernetes with Kapitan
Home Assistant Core, the home automation server, with its configuration and database on a claim, designed in kadet-hub on the project’s image and rendered by the kadet-hub kubernetes generator 1.14.0.
- Latest version:
1.0.0, published - Source: container image
ghcr.io/home-assistant/home-assistant:2026.11.0@sha256:c6c1ca9931036bf3a2c4cc088d8db25dfea3b33935a5ecc83c40349462e2f69e, 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.19.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 88 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 2, HIGH 43, MEDIUM 30, LOW 11, UNKNOWN 2. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:ghcr.io/home-assistant/home-assistant.
What it deploys
recipes.home-assistant(init.yml)recipes.home-assistant.home-assistant(home-assistant.yml):home-assistant
Required parameters
No parameter has to be set.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.home-assistant
parameters:
namespace: home-assistant
kapitan:
vars:
target: home-assistant
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/home-assistant:1.0.0@sha256:88a2ac9aecaf2097e6fb446aede66c2ccf56df0da16bcff82a00f215671b00f4
output_path: inventory/classes/recipes/home-assistant
The first command fetches the recipe into inventory/classes/recipes/home-assistant, 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/home-assistant@sha256:88a2ac9aecaf2097e6fb446aede66c2ccf56df0da16bcff82a00f215671b00f4 \
--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
| Setting | Inventory |
|---|---|
| Home Assistant version | components.home-assistant.image, tag and digest |
| time zone of the container’s processes, UTC | components.home-assistant.env.TZ, a tz database name such as Europe/Amsterdam |
size of the claim config, 1Gi |
components.home-assistant.volume_claims.config.spec.resources.requests.storage |
| exposure | components.home-assistant.service.type, or a route or Ingress of the target to the Service home-assistant on port 8123 |
| resources | components.home-assistant.resources |
Home Assistant keeps its state in /config on the claim:
configuration.yaml, the users and tokens in .storage, and the
recorder’s database home-assistant_v2.db. The database grows with the
number of entities and the days of history the recorder keeps, ten by
default. Home Assistant’s own time zone is set in the onboarding and
stored in .storage, apart from TZ.
The recipe creates no account (DEC-133). The first visitor of the web UI creates the owner in the onboarding, without a login. Create the owner before the target exposes the Service. Until then only the cluster reaches it.
The pod has no probe, because neither the image nor its documentation names a check. It is ready once the container runs, before Home Assistant listens on port 8123. Its termination grace period is 60 seconds, the stop timeout of the container guide, so that Home Assistant closes its database. Until Home Assistant has started, a SIGTERM ends it before it writes the changes it holds back, such as a new token. A preStop hook holds the stop until Home Assistant catches SIGTERM (DEC-333).
Behind a route or Ingress, Home Assistant refuses forwarded requests until
its http integration names the proxy’s addresses in
trusted_proxies with use_x_forwarded_for: true in
configuration.yaml.
Left out
Without the option host-network, Home Assistant has no multicast or
broadcast of the LAN. These do not work:
- discovery of devices over mDNS (zeroconf), SSDP and DHCP. Integrations that take a host or address, such as Google Cast’s known hosts, still connect.
- HomeKit. The HomeKit Bridge announces Home Assistant over mDNS, and
HomeKit Device finds accessories over mDNS. For the bridge, Home
Assistant’s HomeKit documentation describes an mDNS reflector on the LAN
together with the option
advertise_ip.
With or without the option, the recipe renders no privileged, no device
and no host path. That leaves out:
- Bluetooth, which needs the host’s D-Bus at
/run/dbus. - USB and serial devices, such as Zigbee and Z-Wave coordinators.
- the host’s
/etc/localtime, for whichTZstands in.
Home Assistant Container has no apps (add-ons), with or without the recipe.
Proof
check home-assistant --level deploys with tests/target.yml, on kind for
each Kubernetes minor CI tests. The image has no Kubernetes objects to
compare with, so the recipe is proven by deploying it (RCP-44).
tests/smoke checks that the pod passes Pod Security baseline. It then
starts a pod of Home Assistant’s image, which carries curl. From there,
through the Service home-assistant on port 8123, it
- requires that no onboarding step is done, creates the owner through
/api/onboarding/usersand exchanges the code it returns at/auth/tokenfor an access and a refresh token - gets 403 for a second owner
- gets 401 from
/api/without a token andAPI running.with the access token, and requires that/api/confignames the image tag asversion - deletes the pod
home-assistant-0and, once Home Assistant answers again, finds the owner’s step done, gets a new access token with the refresh token andAPI running.with it, which needs.storageon the claim.
The option host-network is proven by its render only: compiled with
tests/target.yml, it changes nothing but hostNetwork and dnsPolicy,
and check home-assistant --level renders passes with it. CI deploys the
recipe without it.
Not tested: the web UI in a browser, an integration, a device, and a route or Ingress in front of the Service.
Versions
1.0.0, :sha256:88a2ac9aecaf2097e6fb446aede66c2ccf56df0da16bcff82a00f215671b00f4
The recipe's files are in kadet-hub/recipes. All recipes