Skip to content

How-to Guides

Scope

Task recipes for Flux 2.9: install the CLI, bootstrap with the CLI or the Flux Operator, upgrade across removed APIs, deliver from Git and OCI, run Helm releases, automate image updates, decrypt SOPS secrets, lock down multi-tenancy, template apps with ResourceSets, handle monorepos, wire webhooks and alerts, and troubleshoot. Commands were checked against the flux2 v2.9.5 CLI source. For API versions and flags see the Reference; for the why, see the Explanation.

Install the Flux CLI

# Homebrew (macOS/Linux)
brew install fluxcd/tap/flux

# Install script (macOS/Linux)
curl -s https://fluxcd.io/install.sh | sudo bash

# Container image with kubectl + flux
docker run --rm ghcr.io/fluxcd/flux-cli:v2.9.5 version --client

# Check the cluster meets the prerequisites and whether a newer Flux exists
flux check --pre

Other channels: mise use -g flux2@latest, yay -S flux-bin, nix-env -i fluxcd, choco install flux (Flux installation). The CLI is supported within one minor version of the controllers.

Bootstrap Flux

Bootstrap with the Flux CLI (GitHub)

flux bootstrap creates or reuses the repository, commits the Flux manifests under --path, installs the controllers, and creates the root flux-system GitRepository and Kustomization.

export GITHUB_TOKEN=<personal-access-token>

# Repository owned by an organization
flux bootstrap github \
  --owner=my-org \
  --repository=fleet-infra \
  --branch=main \
  --path=clusters/production

# Repository on a personal account
flux bootstrap github \
  --owner=my-user \
  --repository=fleet-infra \
  --branch=main \
  --path=clusters/production \
  --personal

# Include image automation and source-watcher (ArtifactGenerator)
flux bootstrap github \
  --owner=my-org --repository=fleet-infra --path=clusters/production \
  --components-extra=image-reflector-controller,image-automation-controller,source-watcher

flux check

Equivalent subcommands exist for gitlab, gitea, bitbucket-server, and generic git. Since 2.9 you can also bootstrap onto AWS CodeCommit with Workload Identity and sign bootstrap commits with SSH keys.

Bootstrap with the Flux Operator

The Flux Operator replaces the bootstrap commit loop with a declarative FluxInstance. Use it when you manage fleets, want automated upgrades, or want the Flux Web UI.

helm install flux-operator oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator \
  --namespace flux-system --create-namespace

# Credentials for a private repository
flux create secret git flux-system \
  --url=https://github.com/my-org/fleet-infra.git \
  --username=git \
  --password=$GITHUB_TOKEN
apiVersion: fluxcd.controlplane.io/v1
kind: FluxInstance
metadata:
  name: flux            # must be named flux
  namespace: flux-system
spec:
  distribution:
    version: "2.x"      # track the latest 2.x release
    registry: "ghcr.io/fluxcd"
  components:
    - source-controller
    - kustomize-controller
    - helm-controller
    - notification-controller
  cluster:
    type: kubernetes
    multitenant: false
    networkPolicy: true
  sync:
    kind: GitRepository
    url: "https://github.com/my-org/fleet-infra.git"
    ref: "refs/heads/main"
    path: "clusters/production"
    pullSecret: "flux-system"
kubectl apply -f flux-instance.yaml
kubectl -n flux-system get fluxreport/flux -o yaml     # health, versions, sync status
kubectl -n flux-system port-forward svc/flux-operator 9080:9080   # Flux Web UI

Existing bootstrapped clusters can be migrated; follow the operator's migration guide.

Upgrade Flux

Migrate removed APIs first

Flux 2.8 removed source/v1beta2, kustomize/v1beta2 and helm/v2beta2; Flux 2.9 removed image/v1beta2 and notification/v1beta2. Objects stored with those versions block the upgrade. Follow Upgrade Procedure for Flux v2.7+.

# 1. Rewrite manifests in Git to the latest API versions
flux migrate -f . --extensions=.yml,.yaml,.tpl
git commit -am "Migrate Flux APIs" && git push

# 2. Migrate objects stored in etcd (needs cluster-admin)
flux migrate

# 3a. Bootstrapped cluster: re-run bootstrap with the new CLI
flux bootstrap github --owner=my-org --repository=fleet-infra --path=clusters/production

# 3b. GitOps-only upgrade: regenerate the components file and let Flux apply it
flux install --export > ./clusters/production/flux-system/gotk-components.yaml
git commit -am "Update to $(flux -v)" && git push
flux reconcile ks flux-system --with-source

# 4. Verify
flux check

With the Flux Operator, upgrade the operator (Helm chart, or a ResourceSet that tracks it) and the FluxInstance picks up the new 2.x release on its own. Review breaking changes before 2.9: the default Helm post-render strategy changed from nohooks to combined, and GCR Receivers now need email and audience in their Secret.

Deliver from Git

# Git source (public or with --secret-ref)
flux create source git podinfo \
  --url=https://github.com/stefanprodan/podinfo \
  --branch=master \
  --interval=1m

# Kustomization that applies a path from that source
flux create kustomization podinfo \
  --source=GitRepository/podinfo \
  --path="./kustomize" \
  --prune=true \
  --wait=true \
  --interval=60m \
  --retry-interval=2m \
  --health-check-timeout=3m \
  --target-namespace=default

# Export instead of applying, to commit the YAML to Git
flux create kustomization podinfo --source=GitRepository/podinfo --path="./kustomize" \
  --prune=true --interval=60m --export > podinfo-ks.yaml

Authenticate with a GitHub App

flux create secret githubapp ghapp-secret \
  --app-id=1 \
  --app-installation-owner=my-org \
  --app-private-key=~/private-key.pem

Then set spec.provider: github and spec.secretRef.name: ghapp-secret on the GitRepository.

Order Deployments with Dependencies

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 60m
  retryInterval: 2m
  timeout: 5m
  prune: true
  wait: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./apps/production
  dependsOn:
    - name: infrastructure    # wait until infrastructure is Ready

Deliver from OCI Artifacts

Push manifests from CI as an OCI artifact, sign it, and pull it in the cluster. No Git credentials are needed in the cluster.

# In CI: push, tag, and sign
flux push artifact oci://ghcr.io/my-org/manifests/app:$(git rev-parse --short HEAD) \
  --path="./deploy" \
  --source="$(git config --get remote.origin.url)" \
  --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)"

flux tag artifact oci://ghcr.io/my-org/manifests/app:$(git rev-parse --short HEAD) --tag=latest

cosign sign --yes ghcr.io/my-org/manifests/app@<digest>
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
  name: app
  namespace: flux-system
spec:
  interval: 5m
  url: oci://ghcr.io/my-org/manifests/app
  ref:
    tag: latest
  verify:
    provider: cosign
    matchOIDCIdentity:
      - issuer: "^https://token.actions.githubusercontent.com$"
        subject: "^https://github.com/my-org/app.*$"
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: app
  namespace: flux-system
spec:
  interval: 60m
  prune: true
  sourceRef:
    kind: OCIRepository
    name: app
  path: ./
# CLI equivalent for a public artifact, pinned by semver
flux create source oci podinfo \
  --url=oci://ghcr.io/stefanprodan/manifests/podinfo \
  --tag-semver=">=6.0.0" \
  --interval=10m

Manage Helm Releases

Prefer OCI charts referenced with chartRef. The HelmRelease upgrades automatically when the chart digest changes.

apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
  name: podinfo
  namespace: apps
spec:
  interval: 10m
  url: oci://ghcr.io/stefanprodan/charts/podinfo
  ref:
    semver: ">=6.0.0"
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: podinfo
  namespace: apps
spec:
  interval: 30m
  chartRef:
    kind: OCIRepository
    name: podinfo
  install:
    remediation:
      retries: 3
  upgrade:
    remediation:
      retries: 3
      remediateLastFailure: true
  driftDetection:
    mode: enabled
    ignore:
      - paths: ["/spec/replicas"]
        target:
          kind: Deployment
  valuesFrom:
    - kind: ConfigMap
      name: podinfo-values
  values:
    replicaCount: 2

Classic HTTP Helm repositories still work with HelmRepository + spec.chart.spec:

flux create source helm podinfo \
  --url=https://stefanprodan.github.io/podinfo \
  --interval=1h

flux create helmrelease podinfo \
  --source=HelmRepository/podinfo \
  --chart=podinfo \
  --chart-version=">=6.0.0" \
  --target-namespace=apps \
  --create-target-namespace \
  --values=./values.yaml

Helm v4 behaviour (Flux 2.8+)

New releases use server-side apply and kstatus health checks. Existing releases keep client-side apply until you opt in. To keep Helm 3 behaviour everywhere, start helm-controller with --feature-gates=UseHelm3Defaults=true. If a chart's hooks break under the 2.9 default post-render strategy, set spec.postRenderStrategy: nohooks.

flux reconcile hr podinfo -n apps --with-source   # fetch chart and upgrade now
flux reconcile hr podinfo -n apps --force         # one-off forced upgrade
flux reconcile hr podinfo -n apps --reset         # reset exhausted remediation retries
flux debug hr podinfo -n apps --show-values       # final merged values

Automate Image Updates

Requires the image controllers (--components-extra=image-reflector-controller,image-automation-controller). The APIs are GA (image.toolkit.fluxcd.io/v1) since Flux 2.7; v1beta2 was removed in 2.9.

apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
  name: podinfo
  namespace: flux-system
spec:
  image: ghcr.io/stefanprodan/podinfo
  interval: 5m
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
  name: podinfo
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: podinfo
  policy:
    semver:
      range: ">=6.0.0 <7.0.0"
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
  name: flux-system
  namespace: flux-system
spec:
  interval: 30m
  sourceRef:
    kind: GitRepository
    name: flux-system
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxcdbot@users.noreply.github.com
        name: fluxcdbot
    push:
      branch: main
  update:
    path: ./clusters/production
    strategy: Setters

Mark the field to update in the Deployment manifest:

image: ghcr.io/stefanprodan/podinfo:6.7.0 # {"$imagepolicy": "flux-system:podinfo"}
flux create image repository podinfo --image=ghcr.io/stefanprodan/podinfo --interval=5m
flux create image policy podinfo --image-ref=podinfo --select-semver=">=6.0.0"
flux get images all -A
flux reconcile image repository podinfo

Decrypt Secrets with SOPS

This uses age keys and the per-Kustomization spec.decryption setting (Flux SOPS guide).

# 1. Generate an age key and store the private key in the cluster
age-keygen -o age.agekey
kubectl -n flux-system create secret generic sops-age \
  --from-file=age.agekey=age.agekey

# 2. Encrypt only data fields, with the public key
sops --age=<age-public-key> \
  --encrypt --encrypted-regex '^(data|stringData)$' \
  --in-place clusters/production/secrets/db.yaml
# 3. Tell the Kustomization to decrypt
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: secrets
  namespace: flux-system
spec:
  interval: 60m
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./clusters/production/secrets
  decryption:
    provider: sops
    secretRef:
      name: sops-age

Key management

Anyone who can read sops-age can decrypt every secret in the repository. Restrict it with RBAC, keep a backup of the private key outside the cluster, and prefer cloud KMS with Workload Identity (spec.decryption.serviceAccountName, Flux 2.7+) or OpenBao/Vault Kubernetes auth (Flux 2.9+) so no key material lives in the cluster. Controller-global age decryption also exists (Flux 2.7+) if you want one key for all Kustomizations.

Lock Down Multi-Tenancy

Apply these patches in clusters/<name>/flux-system/kustomization.yaml before (or when re-running) bootstrap (Flux multi-tenancy):

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - gotk-components.yaml
  - gotk-sync.yaml
patches:
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-remote-bases=true
    target:
      kind: Deployment
      name: "kustomize-controller"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --default-service-account=default
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller)"
  - patch: |
      - op: add
        path: /spec/serviceAccountName
        value: kustomize-controller
    target:
      kind: Kustomization
      name: "flux-system"

Onboard a tenant (namespace, ServiceAccount, RoleBinding) and generate its manifests for Git:

flux create tenant team-a \
  --with-namespace=team-a \
  --cluster-role=admin \
  --export > tenants/team-a/rbac.yaml

With the Flux Operator, set spec.cluster.multitenant: true on the FluxInstance instead of patching. A worked example lives in fluxcd/flux2-multi-tenancy.

Template Apps with ResourceSets

A ResourceSet (Flux Operator) stamps out one HelmRelease + OCIRepository per input. It fills the role of an Argo CD ApplicationSet.

apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
  name: podinfo
  namespace: default
spec:
  inputs:
    - tenant: "team1"
      app:
        version: "6.7.x"
        replicas: 2
    - tenant: "team2"
      app:
        version: "6.6.x"
        replicas: 3
  resources:
    - apiVersion: source.toolkit.fluxcd.io/v1
      kind: OCIRepository
      metadata:
        name: podinfo-<< inputs.tenant >>
        namespace: default
      spec:
        interval: 10m
        url: oci://ghcr.io/stefanprodan/charts/podinfo
        ref:
          semver: << inputs.app.version | quote >>
    - apiVersion: helm.toolkit.fluxcd.io/v2
      kind: HelmRelease
      metadata:
        name: podinfo-<< inputs.tenant >>
        namespace: default
      spec:
        interval: 1h
        releaseName: podinfo-<< inputs.tenant >>
        chartRef:
          kind: OCIRepository
          name: podinfo-<< inputs.tenant >>
        values:
          replicaCount: << inputs.app.replicas | int >>
kubectl apply -f podinfo-resourceset.yaml
kubectl wait resourceset/podinfo --for=condition=ready --timeout=5m
kubectl events --for resourceset/podinfo

For per-pull-request preview environments, pair it with a ResourceSetInputProvider (GitHub PR guide).

Let Other Controllers Own Fields

Stop Flux from reverting fields that an HPA or webhook manages (Flux 2.9+):

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: podinfo
  namespace: flux-system
spec:
  # ...
  ignore:
    - target:
        kind: Deployment
        name: podinfo
      paths:
        - "/spec/replicas"

Always scope rules with target; a rule without one applies to every object in the Kustomization. Escape / inside keys as ~1 (for example /metadata/annotations/external-dns.alpha.kubernetes.io~1hostname).

Handle a Monorepo

Pick the lightest fix that works:

  1. Ignore paths to shrink the artifact (the clone is still full):

    apiVersion: source.toolkit.fluxcd.io/v1
    kind: GitRepository
    metadata:
      name: monorepo
      namespace: flux-system
    spec:
      interval: 1m
      url: https://github.com/org/monorepo
      ref:
        branch: main
      ignore: |
        # exclude everything
        /*
        # include only the deploy directory
        !/deploy/
    
  2. Sparse checkout so only the listed directories are fetched into the artifact: spec.sparseCheckout: [deploy].

  3. Split sources per team or service: several GitRepository objects with narrow ignore rules.
  4. Decompose with ArtifactGenerator (source-watcher) so each app gets its own ExternalArtifact revision and only changed apps reconcile:

    apiVersion: source.extensions.fluxcd.io/v1beta1
    kind: ArtifactGenerator
    metadata:
      name: monorepo-apps
      namespace: flux-system
    spec:
      sources:
        - alias: monorepo
          kind: GitRepository
          name: monorepo
      pathPattern: "@monorepo/apps/{app}/envs/{env}"   # directory discovery, Flux 2.9+
      artifacts:
        - name: "{app}-{env}"
          copy:
            - from: "@monorepo/apps/{app}/envs/{env}/**"
              to: "@artifact/"
    

    Consume each artifact with a Kustomization whose sourceRef is kind: ExternalArtifact, name: <app>-<env>.

  5. Build OCI artifacts in CI (see Deliver from OCI Artifacts); this is the most scalable pattern for very large repositories.

Set Up Webhooks and Alerts

Trigger reconciliation on push instead of waiting for the poll interval:

TOKEN=$(head -c 12 /dev/urandom | shasum | cut -d ' ' -f1)
kubectl -n flux-system create secret generic receiver-token --from-literal=token=$TOKEN

flux create receiver github-receiver \
  --type=github \
  --event=ping --event=push \
  --secret-ref=receiver-token \
  --resource=GitRepository/flux-system

flux get receivers   # shows the webhook path to configure in GitHub

Expose the webhook-receiver Service (port 9292 on notification-controller) through an Ingress or Gateway. In CI, flux trigger receiver <name> --url=<receiver-url> --token=<token> calls a generic receiver without hand-crafting the request (Flux 2.9+).

Send failures to Slack:

kubectl -n flux-system create secret generic slack-token --from-literal=token=<bot-token>
flux create alert-provider slack --type=slack --channel=gitops-alerts --secret-ref=slack-token
flux create alert on-call --provider-ref=slack --event-severity=error \
  --event-source='Kustomization/*' --event-source='HelmRelease/*'

Extend the CLI with Plugins

Flux 2.9 added a plugin system (RFC-0013). Plugins install to ~/fluxcd/plugins and run as flux <plugin>.

flux plugin search
flux plugin install schema@0.5.0       # pin a version, or @sha256:<digest> for CI
flux plugin install mirror
flux plugin list
flux plugin update schema
flux plugin uninstall schema

schema validates manifests against Kubernetes, OpenShift, Gateway API and Flux schemas plus CEL rules; mirror copies Helm charts, OCI artifacts and images between registries. ControlPlane publishes an operator plugin for Flux Operator resources.

Troubleshooting

# Overall status
flux check
flux get all -A
flux get sources git -A
flux get kustomizations -A --status-selector ready=false
flux get helmreleases -A

# Why did it fail?
flux events --for Kustomization/apps -n flux-system
flux logs --kind=Kustomization --name=apps --since=1h
flux debug ks apps --show-status
flux debug ks apps --show-history
flux tree kustomization flux-system --compact

# Preview and render locally before pushing
flux diff kustomization apps --path ./apps/production
flux build kustomization apps --path ./apps/production --dry-run

# Force action
flux reconcile source git flux-system
flux reconcile kustomization apps --with-source

# Pause and resume
flux suspend kustomization apps
flux resume kustomization apps

# Export for backup or review
flux export source git --all > sources.yaml
flux export kustomization --all > kustomizations.yaml
Issue Diagnosis Fix
Source not ready flux get sources git -A, flux events --for GitRepository/<name> Check URL, credentials Secret, known_hosts, provider (GitHub App, Workload Identity)
Kustomization failed build flux build kustomization <name> --path ... Fix YAML, missing components (spec.ignoreMissingComponents), unresolved ${VAR} (strict substitution is on by default since Flux 2.9)
Kustomization stuck on health check flux debug ks <name> --show-status Fix the failing workload; raise spec.timeout; enable CancelHealthCheckOnNewRevision to pick up fixes faster
HelmRelease stuck / retries exhausted flux get hr -A, flux debug hr <name> --show-history Fix values or chart version, then flux reconcile hr <name> --reset
Helm hooks break after 2.9 upgrade Events mention post-render Set spec.postRenderStrategy: nohooks
Image not updating flux get images all -A Check ImagePolicy range and registry auth, setter marker namespace:name, push permissions
Upgrade fails with unknown API version CRD validation error on v1beta* Run flux migrate and flux migrate -f ., then re-apply
Resources flapping between Flux and HPA/webhook Repeated drift events Add spec.ignore rules (2.9+) or HelmRelease driftDetection.ignore

Sources