Flux
Overview
Section titled “Overview”Flux is a tool that ensures a cluster stays in synchronization with a Git
repository. It could be any Git repository, be it Forgejo, GitHub, git daemon, or even a bare repo over SSH.
The reason to implement this earlier rather than later is that it can:
- Declaratively manage Helm charts (see Helm and Flux Integration for a worked example)
- Transparently handle SOPS encrypted secrets
- It is in fact quite necessary to use SOPS with Flux, otherwise you would need some out-of-band process for managing secrets. By registering an Age key in the cluster, Flux can use that so that if secrets change it will just decrypt them in an ordinary way. See Secrets Management for the full setup.
- Use one age key per cluster.
- SOPS supports multiple recipients, so encrypt each secret to multiple age keys at once.
- Reconcile state
- Container images
- Check for updates
- Apply, rollout, fallback
- Logging (use loki here)
- Update manifest with current image
- Helm charts
- Check for updates
- Apply, rollout etc.
- Use semver ranges in
HelmRelease.spec.chart.spec.version(e.g.^1.0.0) to auto-upgrade within a major version - Flux updates the running release, but does not commit the resolved version back to Git — use Renovate if you want manifest-level pinning
- Pulls from
gitevery few minutes- Pulls change, rollout etc.
- Drift
- Changes to a cluster will be reverted and pulled back in synchronization with Git.
- I think this is arguably the most valuable feature of all. Even though getting updates from Git seems valuable and undoing changes somebody applied seems undesirable, Enforcing lockstep with a declarative configuration means that everybody knows exactly how the current setup works and it can be reproduced.
- Notifications
- The notification controller can push alerts to Slack, Microsoft Teams, Discord, or any other arbitrary webhook when reconciliation succeeds or fails. So you get visibility without the pain of something like Vercel.
- Health Checks
- Flux can wait for one resource to be ready before deploying the next. You don’t have to deal with explaining to the container how to apply the latest CI, CD. Even if Kubernetes does a lot for you, this is finished. You just add it and you’re done. Call it a day.
Bootstrap
Section titled “Bootstrap”Pre-Requisites
Section titled “Pre-Requisites”One must have k0s running a cluster, flux will do the rest
Local Files
Section titled “Local Files”Use flux bootstrap git to bootstrap from any local or self-hosted Git repository. You’ll need a bare repo accessible over SSH or a local Git server.
Bare Repo over SSH
Section titled “Bare Repo over SSH”# Create a bare repo from your working repogit clone --bare /path/to/your/repo /path/to/repos/my-repo.git
# Add it as a remote in your working repocd /path/to/your/repogit remote add local /path/to/repos/my-repo.git
# Bootstrap Flux against itflux bootstrap git \ --url=ssh://localhost/path/to/repos/my-repo.git \ --branch=main \ --private-key-file=~/.ssh/id_ed25519 \ --path=clusters/my-clusterLocal Git Daemon (no SSH key needed)
Section titled “Local Git Daemon (no SSH key needed)”# Serve your repo over git protocolgit daemon --rw --base-path=/path/to/repos --export-all
# Bootstrap against itflux bootstrap git \ --url=git://localhost/my-repo.git \ --branch=main \ --path=clusters/my-cluster- The
--pathflag scopes the cluster sync to a specific directory in the repo, allowing multiple clusters from one repo. - Bootstrap is idempotent — safe to run as many times as needed.
- After bootstrap, all cluster operations (including Flux upgrades) can be done via
git push.
Official docs: Flux bootstrap for Git servers
Forgejo
Section titled “Forgejo”Forgejo is a fork of Gitea. Flux supports Gitea natively via flux bootstrap gitea, and Forgejo is backward compatible with this command.
Token Auth (HTTPS)
Section titled “Token Auth (HTTPS)”export GITEA_TOKEN=<your-forgejo-pat>
flux bootstrap gitea \ --token-auth \ --hostname=https://forgejo.example.com:3000 \ --owner=<user-or-org> \ --repository=fleet-infra \ --branch=main \ --path=clusters/my-cluster \ --personalSSH Auth
Section titled “SSH Auth”For existing repos, use flux bootstrap git with an SSH key:
flux bootstrap git \ --url=ssh://git@forgejo.example.com/<org>/<repository> \ --branch=main \ --private-key-file=<path/to/private.key> \ --password=<key-passphrase> \ --path=clusters/my-clusterSelf-Signed Certificates
Section titled “Self-Signed Certificates”flux bootstrap gitea \ --hostname=https://forgejo.example.com:3000 \ --owner=<user> \ --repository=fleet-infra \ --branch=main \ --path=clusters/my-cluster \ --personal \ --ca-file=./forgejo-ca.crt- Forgejo requires HTTPS for token auth —
flux bootstrap giteawill reject plain HTTP. - For HTTP-only Forgejo instances, use
flux bootstrap gitwith--allow-insecure-http=true --token-auth=true. - The Forgejo PAT is stored as a Kubernetes Secret named
flux-systemin theflux-systemnamespace. - SSH deploy keys are linked to the PAT — if the PAT is revoked, the deploy key stops working.
Official docs: Flux bootstrap for Gitea
GitHub
Section titled “GitHub”Commands
Section titled “Commands”flux create source git my-app \ --url=ssh://git@github.com/myorg/private-repo.git \ --branch=main \ --secret-ref=my-ssh-keyKubectl
Section titled “Kubectl”kubectl create secret generic my-ssh-key \ --from-file=identity=~/.ssh/id_ed25519 \ --from-file=known_hosts=~/.ssh/known_hostsFull Bootstrap
Section titled “Full Bootstrap”export GITHUB_TOKEN=<your-pat>
flux bootstrap github \ --owner=<org-or-user> \ --repository=fleet-infra \ --branch=main \ --path=clusters/my-cluster \ --personal- If the repository doesn’t exist, Flux creates it as private by default.
- Use
--token-auth=falseto use SSH deploy keys instead of storing the PAT in-cluster. - Fine-grained PATs need “Contents” read/write and “Administration” read/write permissions.
Official docs: Flux bootstrap for GitHub
Secrets
Section titled “Secrets”Overview
Section titled “Overview”One must generate an Age key and register it as a secret in the Kubernetes cluster. If one has multiple clusters, one should have a separate age key for each cluster as Flux is cluster scoped. See Secrets Management for the full walkthrough.
So on you can run a command approximately similar to this:
age-keygen | tee kubectl create secret generic sops-age \ --namespace=flux-system \ --from-file=age.agekey=/dev/stdinStore the key in KeePass, and then the SOPS encrypted secrets in the repository will be transparently dealt with by Flux.
One can also achieve this in typescript:
import { generateIdentity, identityToRecipient } from "age-encryption";
const secretKey = await generateIdentity(); // AGE-SECRET-KEY-1...const publicKey = await identityToRecipient(secretKey); // age1...
// Now you can handle them however you wantconst keyPair = { secretKey, // store in KeePass / cluster secret publicKey, // put in .sops.yaml};Image Automation
Section titled “Image Automation”Flux can scan container registries for new tags, select the latest according to a policy, and commit the updated image reference back to Git. This requires two extra controllers that are not installed by default.
Bootstrap with Image Controllers
Section titled “Bootstrap with Image Controllers”Re-bootstrap with --components-extra:
flux bootstrap github \ --components-extra=image-reflector-controller,image-automation-controller \ --owner=RyanGreenup \ --repository=kubernetes-template \ --branch=main \ --path=clusters/vale \ --read-write-key \ --personal--read-write-key is essential — the automation controller needs push access to
commit image updates back to Git.
Three Resources
Section titled “Three Resources”The pipeline has three resources, which form a chain:
ImageRepository → ImagePolicy → ImageUpdateAutomation (scan registry) (select tag) (commit to Git)1. ImageRepository — what to scan
Section titled “1. ImageRepository — what to scan”apiVersion: image.toolkit.fluxcd.io/v1kind: ImageRepositorymetadata: name: podinfo namespace: flux-systemspec: image: ghcr.io/stefanprodan/podinfo interval: 5m # For private registries: # secretRef: # name: regcred # kubernetes.io/dockerconfigjson type exclusionList: - "^.*\\.sig$".spec.image— registry address without scheme (docker.io/library/nginx,ghcr.io/org/app).spec.interval— how often to poll.spec.secretRef— for private registries, reference akubernetes.io/dockerconfigjsonsecret.spec.provider—generic(default),aws,azure, orgcpfor native cloud auth.spec.exclusionList— regex patterns for tags to skip (defaults to["^.*\\.sig$"]to exclude cosign signatures)
2. ImagePolicy — which tag to select
Section titled “2. ImagePolicy — which tag to select”Three strategies: semver, alphabetical, numerical.
Semver (most useful):
apiVersion: image.toolkit.fluxcd.io/v1kind: ImagePolicymetadata: name: podinfo namespace: flux-systemspec: imageRepositoryRef: name: podinfo policy: semver: range: ">=1.0.0 <2.0.0" # auto-update minor+patch, pin major to 1Semver range cheat sheet:
| Range | Meaning |
|---|---|
^1.0.0 | >=1.0.0 <2.0.0 — minor + patch within 1.x |
~1.2.0 | >=1.2.0 <1.3.0 — patch only within 1.2.x |
5.0.x | patch only within 5.0 |
>=1.0.0 <2.0.0 | explicit form of ^1.0.0 |
* | any version (dangerous) |
Tag filtering (extract semver from complex tags like 6.0.0-alpine3.12):
spec: filterTags: pattern: '^(?P<semver>[0-9]+\.[0-9]+\.[0-9]+)-(alpine.*)' extract: "$semver" policy: semver: range: ">=6.0.0"3. ImageUpdateAutomation — how to commit
Section titled “3. ImageUpdateAutomation — how to commit”apiVersion: image.toolkit.fluxcd.io/v1kind: ImageUpdateAutomationmetadata: name: flux-system namespace: flux-systemspec: interval: 30m sourceRef: kind: GitRepository name: flux-system git: checkout: ref: branch: main commit: author: email: fluxcdbot@users.noreply.github.com name: fluxcdbot messageTemplate: | Automated image update
{{range .Changed.Changes}} {{print .OldValue}} -> {{println .NewValue}} {{end}} push: branch: main # push to a different branch for PR workflows update: path: ./clusters/vale strategy: Setters.spec.update.strategymust beSetters— this tells the controller to look for marker comments in YAML files.spec.update.path— directory to scan for markers (scope it to avoid scanning the whole repo).spec.policySelector— limit which ImagePolicies this automation considers (useful for excluding detection-only policies, see below)
Image Markers
Section titled “Image Markers”The automation controller finds fields to update via inline JSON comments in your YAML manifests. Add these to Deployment specs, HelmRelease values, etc.
Plain Deployment:
spec: containers: - name: app image: ghcr.io/stefanprodan/podinfo:5.0.0 # {"$imagepolicy": "flux-system:podinfo"}HelmRelease values (tag and name separated):
apiVersion: helm.toolkit.fluxcd.io/v2kind: HelmReleasemetadata: name: podinfospec: values: image: repository: ghcr.io/stefanprodan/podinfo # {"$imagepolicy": "flux-system:podinfo:name"} tag: 5.0.0 # {"$imagepolicy": "flux-system:podinfo:tag"}Marker suffixes: :name (repository only), :tag (tag only), :digest
(digest only), or none (full image:tag).
Auto-Update Minor, Alert on Major
Section titled “Auto-Update Minor, Alert on Major”Flux doesn’t have a single “update minor but alert on major” switch. Instead, combine two ImagePolicies with the notification controller.
Step 1 — Policy that auto-updates (pinned to current major):
apiVersion: image.toolkit.fluxcd.io/v1kind: ImagePolicymetadata: name: myapp namespace: flux-systemspec: imageRepositoryRef: name: myapp policy: semver: range: ">=1.0.0 <2.0.0" # auto-updates 1.x.yStep 2 — Detection-only policy (wide range, labelled):
apiVersion: image.toolkit.fluxcd.io/v1kind: ImagePolicymetadata: name: myapp-latest namespace: flux-system labels: purpose: detection-onlyspec: imageRepositoryRef: name: myapp policy: semver: range: ">=1.0.0" # matches 2.0.0+ tooStep 3 — Exclude detection policy from automation:
apiVersion: image.toolkit.fluxcd.io/v1kind: ImageUpdateAutomationmetadata: name: flux-system namespace: flux-systemspec: # ... (same as above) policySelector: matchExpressions: - key: purpose operator: NotIn values: - detection-onlyStep 4 — Alert when the detection policy selects a new major:
apiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Alertmetadata: name: major-version-alert namespace: flux-systemspec: providerRef: name: slack # or any provider, see Notifications section eventSources: - kind: ImagePolicy name: myapp-latestWhen myapp-latest resolves to 2.0.0, Flux fires an event. The pinned
myapp policy stays on 1.x.y and keeps the cluster on the old major. You
review the alert and bump the range manually.
Helm Chart Auto-Updates
Section titled “Helm Chart Auto-Updates”Helm chart version updates are handled by the source-controller and helm-controller — no extra components needed. This is separate from image automation.
HelmRepository
Section titled “HelmRepository”apiVersion: source.toolkit.fluxcd.io/v1kind: HelmRepositorymetadata: name: bitnami namespace: flux-systemspec: interval: 5m url: https://charts.bitnami.com/bitnamiFor OCI-based charts:
apiVersion: source.toolkit.fluxcd.io/v1kind: HelmRepositorymetadata: name: podinfo namespace: flux-systemspec: type: oci interval: 5m url: oci://ghcr.io/stefanprodan/chartsHelmRelease with Semver Ranges
Section titled “HelmRelease with Semver Ranges”The key is .spec.chart.spec.version. Use a semver range instead of an exact
version and Flux will automatically upgrade when a new matching chart version
appears in the repository index.
apiVersion: helm.toolkit.fluxcd.io/v2kind: HelmReleasemetadata: name: podinfo namespace: defaultspec: interval: 5m chart: spec: chart: podinfo version: ">=1.0.0 <2.0.0" # auto-update minor+patch sourceRef: kind: HelmRepository name: podinfo namespace: flux-system interval: 5m # how often to check for new chart versions # Rollback on failure upgrade: remediation: retries: 3 values: replicaCount: 2Semver range examples for charts:
| Range | Effect |
|---|---|
'6.5.*' | Patch only within 6.5 |
'^1.0.0' | Minor + patch within major 1 |
'>=4.0.0 <5.0.0' | Same as ^4.0.0 |
'*' | Any version — do not use in production |
How It Works
Section titled “How It Works”- The source-controller periodically fetches the HelmRepository index
(controlled by
HelmRepository.spec.interval) - It resolves the best chart version matching the semver range
- If the resolved version differs from the current release, the helm-controller performs a Helm upgrade
- The running release is updated, but the resolved version is not committed back to Git — the HelmRelease YAML always contains the range, not the pinned version
Upgrade Remediation
Section titled “Upgrade Remediation”Configure rollback behaviour when an upgrade fails:
spec: upgrade: remediation: retries: 3 remediateLastFailure: true # rollback to last successful release rollback: cleanupOnFail: true timeout: 5mNotifications
Section titled “Notifications”The notification-controller ships with the default Flux install. It captures events from all Flux controllers and routes them to external systems.
Provider — where to send alerts
Section titled “Provider — where to send alerts”Slack (bot token — recommended):
apiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Providermetadata: name: slack namespace: flux-systemspec: type: slack channel: flux-alerts address: https://slack.com/api/chat.postMessage secretRef: name: slack-token---apiVersion: v1kind: Secretmetadata: name: slack-token namespace: flux-systemstringData: token: xoxb-YOUR-BOT-TOKENGeneric webhook:
apiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Providermetadata: name: webhook namespace: flux-systemspec: type: generic address: https://example.com/webhookGeneric webhook with HMAC:
apiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Providermetadata: name: webhook-hmac namespace: flux-systemspec: type: generic-hmac address: https://example.com/webhook secretRef: name: hmac-secret---apiVersion: v1kind: Secretmetadata: name: hmac-secret namespace: flux-systemstringData: token: "your-hmac-key"Other supported types: discord, msteams, googlechat, pagerduty,
opsgenie, grafana, alertmanager, webex, rocket, azuredevops,
azureeventhub, datadog, lark, matrix, nats, telegram, and more.
Alert — what events to forward
Section titled “Alert — what events to forward”apiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Alertmetadata: name: on-call namespace: flux-systemspec: providerRef: name: slack eventSeverity: info # 'info' (all) or 'error' (failures only) eventSources: - kind: GitRepository name: "*" - kind: Kustomization name: "*" - kind: HelmRelease name: "*" - kind: ImagePolicy name: "*"name: '*'— wildcard, matches all objects of that kindmatchLabels— filter by labels instead of name.spec.inclusionList/.spec.exclusionList— Go regex filters on the event message body
Receiver — inbound webhooks
Section titled “Receiver — inbound webhooks”Trigger immediate reconciliation from external events (e.g. DockerHub push, GitHub webhook):
apiVersion: notification.toolkit.fluxcd.io/v1kind: Receivermetadata: name: dockerhub namespace: flux-systemspec: type: dockerhub # also: github, gitlab, harbor, quay, nexus, gcr secretRef: name: webhook-token resources: - kind: ImageRepository name: myappPutting It All Together — Example
Section titled “Putting It All Together — Example”A complete working setup for an app called webapp with:
- Auto-update container images on minor/patch
- Alert on major version bumps
- Auto-update Helm chart within major version
clusters/vale/├── flux-system/ # bootstrap output├── image-automation.yaml # ImageUpdateAutomation├── image-policies.yaml # ImageRepository + ImagePolicy resources├── helm-releases.yaml # HelmRepository + HelmRelease resources└── notifications.yaml # Provider + Alert resourcesimage-policies.yaml
Section titled “image-policies.yaml”---apiVersion: image.toolkit.fluxcd.io/v1kind: ImageRepositorymetadata: name: webapp namespace: flux-systemspec: image: ghcr.io/myorg/webapp interval: 5m exclusionList: - "^.*\\.sig$"---# Auto-updates minor+patch within current majorapiVersion: image.toolkit.fluxcd.io/v1kind: ImagePolicymetadata: name: webapp namespace: flux-systemspec: imageRepositoryRef: name: webapp policy: semver: range: ">=1.0.0 <2.0.0"---# Detection-only — fires alert when major 2 appearsapiVersion: image.toolkit.fluxcd.io/v1kind: ImagePolicymetadata: name: webapp-latest namespace: flux-system labels: purpose: detection-onlyspec: imageRepositoryRef: name: webapp policy: semver: range: ">=1.0.0"image-automation.yaml
Section titled “image-automation.yaml”apiVersion: image.toolkit.fluxcd.io/v1kind: ImageUpdateAutomationmetadata: name: flux-system namespace: flux-systemspec: interval: 30m sourceRef: kind: GitRepository name: flux-system git: checkout: ref: branch: main commit: author: email: fluxcdbot@users.noreply.github.com name: fluxcdbot messageTemplate: | Automated image update
{{range .Changed.Changes}} {{print .OldValue}} -> {{println .NewValue}} {{end}} push: branch: main update: path: ./clusters/vale strategy: Setters policySelector: matchExpressions: - key: purpose operator: NotIn values: - detection-onlyhelm-releases.yaml
Section titled “helm-releases.yaml”---apiVersion: source.toolkit.fluxcd.io/v1kind: HelmRepositorymetadata: name: bitnami namespace: flux-systemspec: interval: 10m url: https://charts.bitnami.com/bitnami---apiVersion: helm.toolkit.fluxcd.io/v2kind: HelmReleasemetadata: name: redis namespace: defaultspec: interval: 5m chart: spec: chart: redis version: ">=19.0.0 <20.0.0" sourceRef: kind: HelmRepository name: bitnami namespace: flux-system interval: 10m upgrade: remediation: retries: 3 values: architecture: standalonenotifications.yaml
Section titled “notifications.yaml”---apiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Providermetadata: name: slack namespace: flux-systemspec: type: slack channel: flux-alerts address: https://slack.com/api/chat.postMessage secretRef: name: slack-token---# Alert on major version detectionapiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Alertmetadata: name: major-version-alert namespace: flux-systemspec: providerRef: name: slack eventSources: - kind: ImagePolicy name: webapp-latest---# Alert on all Helm upgrade failuresapiVersion: notification.toolkit.fluxcd.io/v1beta3kind: Alertmetadata: name: helm-errors namespace: flux-systemspec: providerRef: name: slack eventSeverity: error eventSources: - kind: HelmRelease name: "*"References
Section titled “References”- Flux installation overview
- Flux bootstrap command reference
- Flux bootstrap for generic Git servers
- Flux bootstrap for Gitea / Forgejo
- Flux bootstrap for GitHub
- Getting started with Flux
- Image update guide
- ImageRepository API
- ImagePolicy API
- ImageUpdateAutomation API
- HelmRelease API
- Notification controller
- Alert API