--- title: "How to Set Up a GitOps Workflow with Flux CD on Kubernetes" description: "Bootstrap Flux CD on Kubernetes from GitHub, deploy an app from Git with a Kustomization and a Helm chart with a HelmRelease, and manage reconciliation." category: "CI/CD DevOps" language: en source: https://cubepath.com/docs/ci-cd-devops/gitops-workflow-with-flux-cd updated: 2026-09-25 --- # How to Set Up a GitOps Workflow with Flux CD on Kubernetes 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: ```bash kubectl get nodes ``` ```output NAME STATUS ROLES AGE VERSION node-1 Ready control-plane 12d v1.33.4 node-2 Ready 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: ```bash 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`: ```bash sudo bash install-flux.sh rm install-flux.sh ``` Check the installed version: ```bash flux --version ``` ```output flux version 2.7.0 ``` Optionally enable shell completion for Bash: ```bash echo '. <(flux completion bash)' >> ~/.bashrc ``` Before installing anything in the cluster, let Flux check that your Kubernetes version is supported: ```bash flux check --pre ``` ```output ► 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: ```bash 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: ```bash 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: ```output ✔ source-controller: deployment ready ✔ kustomize-controller: deployment ready ✔ helm-controller: deployment ready ✔ notification-controller: deployment ready ✔ all components are healthy ``` Verify the installation: ```bash flux check kubectl get pods -n flux-system ``` Finally, clear the token from your shell session: ```bash unset GITHUB_TOKEN ``` ## Step 3 - Cloning the fleet repository All further changes to the cluster happen through this repository. Clone it on your workstation: ```bash git clone https://github.com/$GITHUB_USER/fleet-infra cd fleet-infra ``` The bootstrap created this layout: ```text 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: ```bash 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: ```bash 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: ```bash cat ./clusters/my-cluster/podinfo-kustomization.yaml ``` ```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: ```bash 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: ```bash flux get kustomizations --watch ``` ```output 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: ```bash kubectl -n default get deployments,services ``` ```output 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 9898/TCP,9999/TCP 1m ``` To see drift correction in action, scale the deployment by hand and then force a reconciliation: ```bash 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: ```bash mkdir -p ./clusters/my-cluster/apps nano ./clusters/my-cluster/apps/podinfo-helm.yaml ``` Add both objects to the file: ```yaml 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: ```bash 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: ```bash flux reconcile kustomization flux-system --with-source flux get helmreleases -n flux-system ``` ```output NAME REVISION SUSPENDED READY MESSAGE podinfo 6.9.0 False True Helm install succeeded for release podinfo-helm/podinfo-helm-podinfo.v1 with chart podinfo@6.9.0 ``` ```bash 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: ```bash flux get all -A ``` Show recent errors from the Flux controllers: ```bash 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: ```bash flux suspend kustomization podinfo flux resume kustomization podinfo ``` See why an object is not ready, including the events recorded by the controller: ```bash 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: ```bash 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 ` 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.