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
kubeconfigwith 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
kubectlconfigured for that cluster andgitinstalled. - A GitHub account and a personal access token. A classic token needs the
reposcope; 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: truedeletes objects from the cluster when you remove them from Git, so the cluster never keeps leftovers.wait: truemakes the Kustomization reportReadyonly after all deployed objects are healthy.intervalis how often Flux reapplies the manifests even if nothing changed in Git, which undoes manual edits made withkubectl.
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 example6.9.0) if you want every upgrade to go through a commit.install.createNamespacecreatespodinfo-helmif it does not exist.- The
remediationsettings retry failed installs and roll back a failed upgrade to the previous release, so a bad chart version does not leave the application broken. valuesreplaces what you would pass withhelm 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.