Flux CD is a set of Kubernetes controllers that keep a cluster in sync with the manifests stored in a Git repository. Instead of running kubectl apply from a pipeline, you commit changes to Git and Flux applies them, detects drift and reverts manual changes. In this tutorial you will install the Flux CLI on Ubuntu 24.04, bootstrap Flux on a cluster using a GitHub repository, deploy an application from Git with a Kustomization and a second one from a Helm chart with a HelmRelease.

Prerequisites

To follow this tutorial you need:

  • A running Kubernetes cluster and a kubeconfig with cluster-admin rights. Any conformant cluster works, for example a CubePath managed Kubernetes cluster or a k3s installation.
  • A workstation or server running Ubuntu 24.04 LTS with kubectl configured for that cluster and git installed.
  • A GitHub account and a personal access token. A classic token needs the repo scope; a fine-grained token needs Administration (read and write), Contents (read and write) and Metadata (read-only) on the repository. Flux uses it once to create the repository and a deploy key.

Confirm that kubectl reaches the cluster:

kubectl get nodes
NAME     STATUS   ROLES           AGE   VERSION
node-1   Ready    control-plane   12d   v1.33.4
node-2   Ready    <none>          12d   v1.33.4

Step 1 - Installing the Flux CLI

The Flux project publishes an install script that downloads the latest flux binary for your platform. Download it first and review it before running it:

curl -fsSL -o install-flux.sh https://fluxcd.io/install.sh
less install-flux.sh

Run it with sudo so it can place the binary in /usr/local/bin:

sudo bash install-flux.sh
rm install-flux.sh

Check the installed version:

flux --version
flux version 2.7.0

Optionally enable shell completion for Bash:

echo '. <(flux completion bash)' >> ~/.bashrc

Before installing anything in the cluster, let Flux check that your Kubernetes version is supported:

flux check --pre
► checking prerequisites
✔ Kubernetes 1.33.4 >=1.32.0-0
✔ prerequisites checks passed

If the check fails, upgrade the cluster or use an older Flux release that supports your Kubernetes version.

Step 2 - Bootstrapping Flux with GitHub

flux bootstrap github does several things in one command: it creates the repository if it does not exist, commits the Flux component manifests to it, installs the controllers in the flux-system namespace, creates a read-only deploy key for the repository and configures Flux to manage itself from that repository. From then on, upgrading or changing Flux is also a Git commit.

Export your GitHub username and token. The token is only needed for the bootstrap:

export GITHUB_USER=your_github_user
export GITHUB_TOKEN=your_personal_access_token

Run the bootstrap. --path is the directory in the repository that belongs to this cluster; using one directory per cluster lets you manage several clusters from the same repository later:

flux bootstrap github \
  --owner="$GITHUB_USER" \
  --repository=fleet-infra \
  --branch=main \
  --path=./clusters/my-cluster \
  --personal

--personal tells Flux that the owner is a user account. For a repository in a GitHub organization, drop --personal and set --owner to the organization name. By default the new repository is private.

The command ends with the health of each controller:

✔ source-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ helm-controller: deployment ready
✔ notification-controller: deployment ready
✔ all components are healthy

Verify the installation:

flux check
kubectl get pods -n flux-system

Finally, clear the token from your shell session:

unset GITHUB_TOKEN

Step 3 - Cloning the fleet repository

All further changes to the cluster happen through this repository. Clone it on your workstation:

git clone https://github.com/$GITHUB_USER/fleet-infra
cd fleet-infra

The bootstrap created this layout:

clusters/
  my-cluster/
    flux-system/
      gotk-components.yaml
      gotk-sync.yaml
      kustomization.yaml

gotk-components.yaml contains the Flux controllers, and gotk-sync.yaml defines a GitRepository that points at this repository and a Kustomization that applies everything under ./clusters/my-cluster. Any manifest you add to that directory is picked up automatically.

Step 4 - Deploying an application from Git with a Kustomization

Flux separates sources (where manifests come from) from reconcilers (what to apply and how). You will deploy podinfo, a small demo application maintained by the Flux team, from its public Git repository.

Generate a GitRepository source. The --export flag prints the YAML instead of applying it, so you can commit it:

flux create source git podinfo \
  --url=https://github.com/stefanprodan/podinfo \
  --branch=master \
  --interval=5m \
  --export > ./clusters/my-cluster/podinfo-source.yaml

Generate the Kustomization that applies the manifests in the repository's ./kustomize directory to the default namespace:

flux create kustomization podinfo \
  --target-namespace=default \
  --source=podinfo \
  --path="./kustomize" \
  --prune=true \
  --wait=true \
  --interval=30m \
  --retry-interval=2m \
  --health-check-timeout=3m \
  --export > ./clusters/my-cluster/podinfo-kustomization.yaml

Review the generated file:

cat ./clusters/my-cluster/podinfo-kustomization.yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 30m0s
  path: ./kustomize
  prune: true
  retryInterval: 2m0s
  sourceRef:
    kind: GitRepository
    name: podinfo
  targetNamespace: default
  timeout: 3m0s
  wait: true

The important fields are:

  • prune: true deletes objects from the cluster when you remove them from Git, so the cluster never keeps leftovers.
  • wait: true makes the Kustomization report Ready only after all deployed objects are healthy.
  • interval is how often Flux reapplies the manifests even if nothing changed in Git, which undoes manual edits made with kubectl.

Commit and push:

git add clusters/my-cluster/podinfo-source.yaml clusters/my-cluster/podinfo-kustomization.yaml
git commit -m "Deploy podinfo from Git"
git push

Flux checks the fleet repository every minute. Watch the Kustomizations until podinfo is ready:

flux get kustomizations --watch
NAME          REVISION               SUSPENDED  READY  MESSAGE
flux-system   main@sha1:4f1e2a3b     False      True   Applied revision: main@sha1:4f1e2a3b
podinfo       master@sha1:9c8d7e6f   False      True   Applied revision: master@sha1:9c8d7e6f

Press Ctrl+C to stop watching, then check the deployed objects:

kubectl -n default get deployments,services
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/podinfo   2/2     2            2           1m

NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)             AGE
service/podinfo   ClusterIP   10.43.18.105   <none>        9898/TCP,9999/TCP   1m

To see drift correction in action, scale the deployment by hand and then force a reconciliation:

kubectl -n default scale deployment podinfo --replicas=5
flux reconcile kustomization podinfo --with-source
kubectl -n default get deployment podinfo

The deployment returns to the replica count defined in Git. The change you want must be made in Git, not with kubectl.

Step 5 - Deploying a Helm chart with a HelmRelease

Many applications are distributed as Helm charts. Flux installs and upgrades them with two objects: a HelmRepository source and a HelmRelease that holds the chart version and your values.

Create a directory for the release:

mkdir -p ./clusters/my-cluster/apps
nano ./clusters/my-cluster/apps/podinfo-helm.yaml

Add both objects to the file:

apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 1h
  url: https://stefanprodan.github.io/podinfo
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 30m
  targetNamespace: podinfo-helm
  install:
    createNamespace: true
    remediation:
      retries: 3
  upgrade:
    remediation:
      retries: 3
      remediateLastFailure: true
  chart:
    spec:
      chart: podinfo
      version: "6.x"
      sourceRef:
        kind: HelmRepository
        name: podinfo
  values:
    replicaCount: 2
    resources:
      requests:
        cpu: 50m
        memory: 64Mi

What this does:

  • version: "6.x" is a semantic version range: Flux upgrades automatically to new 6.x chart releases but never to 7.0. Pin an exact version (for example 6.9.0) if you want every upgrade to go through a commit.
  • install.createNamespace creates podinfo-helm if it does not exist.
  • The remediation settings retry failed installs and roll back a failed upgrade to the previous release, so a bad chart version does not leave the application broken.
  • values replaces what you would pass with helm install -f values.yaml.

Commit and push:

git add clusters/my-cluster/apps/podinfo-helm.yaml
git commit -m "Deploy podinfo Helm chart"
git push

Trigger the sync instead of waiting for the next interval, then check the release:

flux reconcile kustomization flux-system --with-source
flux get helmreleases -n flux-system
NAME     REVISION  SUSPENDED  READY  MESSAGE
podinfo  6.9.0     False      True   Helm install succeeded for release podinfo-helm/podinfo-helm-podinfo.v1 with chart [email protected]
kubectl -n podinfo-helm get pods

To change the configuration, edit values in the file (for example, replicaCount: 3), commit and push. Flux runs a Helm upgrade with the new values.

Step 6 - Operating Flux day to day

These commands cover most daily work.

List every Flux object and its status across namespaces:

flux get all -A

Show recent errors from the Flux controllers:

flux logs --level=error --all-namespaces

Pause reconciliation of one object, for example during an incident when you need to make a temporary manual change, and resume it afterwards:

flux suspend kustomization podinfo
flux resume kustomization podinfo

See why an object is not ready, including the events recorded by the controller:

kubectl -n flux-system describe kustomization podinfo

Roll back a bad change by reverting the commit in the fleet repository. Flux applies the previous state on the next reconciliation:

git revert HEAD
git push

To upgrade Flux itself, install the newer CLI and run the same flux bootstrap github command again. It updates gotk-components.yaml in the repository and the controllers apply the new version.

Troubleshooting

The bootstrap fails with 401 Bad credentials or 403 Resource not accessible. The token is wrong, expired or missing permissions. For a fine-grained token, check that it has access to the repository (or to all repositories if Flux has to create it) with the permissions listed in the prerequisites.

A Kustomization shows kustomization path not found. The --path does not exist in the source repository or has a typo. Paths are relative to the repository root, for example ./kustomize.

A Kustomization stays Unknown with Reconciliation in progress and then timeout waiting for. With wait: true, Flux waits for every object to become healthy. Check the workloads it created with kubectl get pods -n <namespace> and kubectl describe the failing Pod.

HelmRelease reports no chart version found. The version range does not match any chart in the repository. List available versions with helm search repo after adding the repository locally, or widen the range.

Conclusion

Flux now manages your cluster from the fleet-infra repository: it deploys applications from Git and from Helm charts, corrects manual drift and records every change in Git history. Next, split the repository into infrastructure and apps Kustomizations with dependsOn so controllers are installed before the applications that need them, encrypt secrets with SOPS before committing them, or add Flux's image automation controllers to update image tags in Git when your CI pushes a new build.