Skip to content

Flux CD

Deploy the Home Assistant Operator and manage its custom resources declaratively with Flux, instead of running helm install / kubectl apply by hand. This is the GitOps pattern: the desired state (operator chart version, HomeAssistant spec, configuration) lives in Git, and Flux continuously reconciles the cluster to match it.

Example layout

A typical setup keeps the operator's HelmRelease and the Home Assistant custom resources together in one directory, applied by a single Flux Kustomization:

clusters/my-cluster/homeassistant/
├── kustomization.yaml
├── namespace.yaml
├── helm-repository.yaml
├── helm-release.yaml
└── resources/
    ├── homeassistant.yaml
    └── configuration.yaml

kustomization.yaml (plain kustomize, not the Flux CRD — this just lists what to apply):

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - namespace.yaml
  - helm-repository.yaml
  - helm-release.yaml
  - resources/homeassistant.yaml
  - resources/configuration.yaml

helm-repository.yaml — the chart is published as an OCI artifact, so HelmRepository just needs type: oci:

apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: homeassistant-operator
  namespace: flux-system
spec:
  type: oci
  interval: 1h
  url: oci://ghcr.io/przemekhys/charts

helm-release.yaml:

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: homeassistant-operator
  namespace: flux-system
spec:
  interval: 10m
  chart:
    spec:
      chart: homeassistant-operator
      version: ">=1.0.0 <2.0.0"
      sourceRef:
        kind: HelmRepository
        name: homeassistant-operator
        namespace: flux-system
  targetNamespace: homeassistant-operator-system
  install:
    createNamespace: true
  values:
    watchNamespaces:
      - homeassistant

resources/homeassistant.yaml and resources/configuration.yaml are just the HomeAssistant and HomeAssistantConfiguration CRs, committed to Git like any other manifest — see Home Assistant CR for the full spec.

Finally, a Flux Kustomization (the CRD, not the plain-kustomize file above) points at this directory:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: homeassistant
  namespace: flux-system
spec:
  interval: 10m0s
  path: ./clusters/my-cluster/homeassistant
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system

Key decisions

OCI HelmRepository, not a classic chart repo. The chart is only published to oci://ghcr.io/przemekhys/charts/homeassistant-operator (see Installation) — there's no index.yaml-based repo, so spec.type: oci is required on the HelmRepository.

Version pinning: ranges are stable-only by default. Once a stable version exists, a semver range like >=1.0.0 <2.0.0 lets Flux pick up minor/patch updates automatically — but a plain range like that will never match a pre-release tag (1.1.0-rc.0), even after one is published. Pre-releases are only matched if the range itself opts in with a pre-release lower bound (e.g. >=1.0.0-0). If you're tracking a single release candidate before it goes stable, it's simplest to just pin the exact version instead of tuning a range for it:

spec:
  chart:
    spec:
      version: "1.1.0-rc.0"   # exact pin — simplest way to track one specific pre-release

Switch back to a plain range once the corresponding stable tag is published.

Custom resources live next to the HelmRelease. Grouping the operator's HelmRelease and its HomeAssistant/HomeAssistantConfiguration CRs in the same directory (and the same Flux Kustomization) keeps "the operator" and "what it manages" as one reconciled unit in Git — one flux diff or flux reconcile covers the whole thing.

Automatic image updates (security-sensitive)

Flux Image Automation can bump spec.version in the HomeAssistant CR automatically whenever a new Home Assistant image is published, instead of you editing it by hand on every release. This is convenient, but it auto-commits to your Git repo — treat the policy that decides which tags qualify as a security control, not just a convenience setting.

Restrict which tags are eligible — never allow pre-releases. Home Assistant publishes dev/beta tags alongside stable calendar-versioned releases (2026.7.1). Without a strict filter, Flux could just as easily pick up a beta build. Combine a filterTags regex with a semver floor so only fully-qualified stable tags are ever considered:

apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
  name: homeassistant
  namespace: flux-system
spec:
  image: ghcr.io/home-assistant/home-assistant
  interval: 1h
  provider: generic
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
  name: homeassistant
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: homeassistant
  filterTags:
    # Stable calendar-versioned tags only (YYYY.MM.patch) — excludes dev/beta
    pattern: '^\d{4}\.\d+\.\d+$'
  policy:
    semver:
      range: ">=2025.0.0"

Wire the policy into the CR with the marker comment Flux looks for:

apiVersion: ha.homeassistant.io/v1
kind: HomeAssistant
metadata:
  name: home
spec:
  version: "2026.7.1" # {"$imagepolicy": "flux-system:homeassistant:tag"}

Then an ImageUpdateAutomation periodically checks for a new match and commits the bump:

apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
  name: homeassistant
  namespace: flux-system
spec:
  interval: 30m
  sourceRef:
    kind: GitRepository
    name: flux-system
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: flux-bot@users.noreply.github.com
        name: flux-bot
      messageTemplate: "chore: auto-update images"
    push:
      branch: main
  update:
    strategy: Setters
    path: ./clusters/my-cluster

Direct push to main vs. a PR gate. The example above pushes the version bump straight to main — no human reviews it before it's live. That's a deliberate trade-off: it's simpler and keeps Home Assistant current with zero manual effort, acceptable for a low-stakes homelab workload where a bad release is easy to roll back (git revert). If you'd rather review every auto-bump before it applies, push to a dedicated branch instead and let a PR (with your normal CI) gate the merge:

spec:
  git:
    push:
      branch: flux-image-updates   # push here instead of main
      # then open/require a PR from flux-image-updates -> main

Don't point Image Automation at the operator's own image. The pattern above is for the workload the operator manages (Home Assistant itself) — not for the operator. The operator's container image is coupled to the Helm chart's appVersion, so there's no independent tag to auto-bump: upgrading the operator means deliberately bumping HelmRelease.spec.chart.spec.version (see Version pinning above). The operator holds cluster-wide RBAC to reconcile CRDs across namespaces — that upgrade deserves a human looking at the changelog, not a silent auto-commit.

See also