Build a GitOps Pipeline with FluxCD
Deploy and manage Kubernetes apps through Git using FluxCD.
Introduction
30 Second Summary
Every time someone manually deploys code to a server, there is a risk of mistakes that are hard to trace. What if every change to your infrastructure had to go through a Git commit, creating an automatic audit trail?
In this project, you will build a GitOps pipeline using FluxCD on a local Kubernetes cluster. You will deploy and customize an application entirely through Git commits, then watch Flux automatically detect and revert unauthorized changes.
What You'll Build
A complete GitOps delivery pipeline where FluxCD continuously reconciles your Kubernetes cluster state with manifests stored in a GitHub repository.
By the end of this project, you'll have:
- A local Kubernetes cluster running FluxCD controllers that watch your Git repository for changes.
- A GitHub repository (fleet-infra) that serves as the single source of truth for all cluster configuration.
- A deployed podinfo application managed entirely through Git commits and customized with Kustomize patches.
- Secret Mission: Deploy a second application using a Flux HelmRelease to manage a Helm chart declaratively through Git.
Are there any prerequisites?
You need basic command line familiarity and a GitHub account. Your Mac should have Docker Desktop, kubectl, and Homebrew already installed.
Step 1 covers installing the two remaining tools (kind and Flux CLI), so no extra setup is needed before you begin.
Set Up Your Local Environment
A GitOps pipeline needs a Kubernetes cluster to manage and a CLI that connects Git to that cluster. Without these, there is no way to demonstrate the pull-based delivery model that FluxCD uses.
In this step, you will install two command-line tools, create a local cluster inside Docker, and configure GitHub access so Flux can read and write your configuration repository.
In this step, get ready to:
- Install kind and the Flux CLI via Homebrew.
- Create a local Kubernetes cluster and confirm Flux prerequisites pass.
- Generate a GitHub Personal Access Token and export your credentials.
Install kind and the Flux CLI
kind (Kubernetes in Docker) spins up a full Kubernetes cluster inside Docker containers. The Flux CLI is the command-line interface for bootstrapping and managing FluxCD on a cluster.
- Install kind by running this command:
brew install kind
- Install the Flux CLI by running this command:
brew install fluxcd/tap/flux
What are these tools?
kind creates lightweight Kubernetes clusters by running each cluster node as a Docker container. It is the tool recommended by Flux's own documentation for local development.
flux is the CLI for FluxCD. You will use it to bootstrap Flux onto your cluster, create resources, and monitor synchronization status.
- Verify both tools installed correctly by running these commands:
kind version
flux --version
- You should see version numbers printed for both tools.
Command not found?
If either command prints command not found, Homebrew may not be on your shell's PATH. Try opening a new terminal window and running the commands again.
If the issue persists, run brew doctor for diagnostics.
Help me troubleshoot why kind or flux is not found after a Homebrew install on macOS.
Create a local Kubernetes cluster
With the tools in place, you can now spin up a cluster. This command downloads a Kubernetes node image and launches it as a Docker container.
- Make sure Docker Desktop is running.
- Create a cluster named flux-demo by running this command:
kind create cluster --name flux-demo
What does this command do?
The --name flux-demo flag gives your cluster a custom name instead of the default kind. This also sets your kubectl context to kind-flux-demo so you can target this cluster with all future commands.
The command takes about a minute because it pulls the Kubernetes node image the first time you run it. You will see progress lines for each setup phase (preparing nodes, writing configuration, starting the control plane, installing networking).
- Verify the cluster is running by checking the cluster info:
kubectl cluster-info --context kind-flux-demo
- You should see output showing the Kubernetes control plane and CoreDNS addresses. This confirms the cluster is healthy.
- Run the Flux pre-flight check to confirm your cluster meets Flux's requirements:
flux check --pre
- You should see a line confirming the Kubernetes version passes and a final prerequisites checks passed message.
Pre-check failing?
If flux check --pre reports a Kubernetes version mismatch, your kind version may be outdated. Run brew upgrade kind to get the latest release, then delete and recreate the cluster.
If kubectl cluster-info cannot connect, confirm Docker Desktop is running and the Docker daemon is responsive.
Help me fix a failing flux check --pre on my kind cluster.
Generate a GitHub Personal Access Token
Flux needs write access to a GitHub repository so it can store cluster configuration. You will create a Personal Access Token that grants this access.
- Open GitHub in your browser.
- Click your profile photo in the top-right corner, then click Settings.
- In the left sidebar, scroll down and click Developer settings.
- Click Personal access tokens, then click Tokens (classic).
- Click Generate new token, then select Generate new token (classic).
- Give the token a descriptive name (e.g. flux-demo).
- Under Select scopes, check the repo checkbox. This grants full control of private repositories.
- Click Generate token.
- Copy the token immediately. GitHub will not show it again.
Why the repo scope?
Flux needs to create a new repository (fleet-infra) on your account and push configuration files to it. The repo scope covers both repository creation and pushing commits.
- Return to your terminal and export your GitHub credentials by running these commands:
export GITHUB_TOKEN=your-token-here
export GITHUB_USER=[[GITHUB_USER="your-github-username"]]
What do these exports do?
These environment variables make your credentials available to the Flux CLI for the remainder of your terminal session. The flux bootstrap command you run in the next step reads both variables automatically.
- Replace your-token-here with the token you just copied from GitHub.
- Replace the username placeholder with your actual GitHub username.
Token not working later?
Environment variables are only available in the terminal session where you exported them. If you close your terminal or open a new tab, you will need to run both export commands again.
My Flux bootstrap is failing with an authentication error even though I exported GITHUB_TOKEN. Help me debug.
Your local environment is ready. Next, you will bootstrap FluxCD onto the cluster and watch it create your GitOps configuration repository on GitHub.
Bootstrap FluxCD onto Your Cluster
Your local Kubernetes cluster is running and Flux passed its pre-flight check. Now it's time to connect Flux to a Git repository so it can start managing your cluster state.
Bootstrapping is the process of installing Flux's controllers onto your cluster and linking them to a GitHub repository. Once bootstrapped, Flux watches that repository for changes and automatically applies them to your cluster.
In this step, get ready to:
- Run the Flux bootstrap command to install controllers and create the fleet-infra repository.
- Verify all Flux controllers are healthy.
- Clone the fleet-infra repository to your local machine.
Run the Flux bootstrap command
The bootstrap command does three things at once: it creates a new GitHub repository, installs the FluxCD controllers onto your cluster, and configures those controllers to watch a specific path in the repository for changes.
- Run the bootstrap command in your terminal:
flux bootstrap github \
--owner=$GITHUB_USER \
--repository=fleet-infra \
--branch=main \
--path=./clusters/my-cluster \
--personal
What does this command do?
- --owner=$GITHUB_USER tells Flux which GitHub account to create the repository under.
- --repository=fleet-infra names the repository that will hold all your cluster configuration.
- --path=./clusters/my-cluster sets the directory inside the repo that Flux watches for manifests to apply.
- --personal tells Flux this is a personal account (not an organization).
Flux reads the GITHUB_TOKEN environment variable you exported earlier to authenticate with GitHub.
The command takes a minute or two to complete. You should see output confirming the repository creation, manifest pushing, and controller installation.
Seeing an authentication error?
- Make sure your GITHUB_TOKEN is still exported. Run echo $GITHUB_TOKEN to confirm it prints a value.
- Check that your token has the repo scope. Tokens without repo access cannot create repositories.
- Confirm GITHUB_USER matches your actual GitHub username exactly (case-sensitive).
Help me troubleshoot a Flux bootstrap authentication error
Verify Flux is running
The bootstrap installed four controllers into the flux-system namespace: source-controller, kustomize-controller, helm-controller, and notification-controller. Each one handles a different part of the GitOps workflow.
- Run the Flux health check to confirm all controllers are healthy:
flux check
- Verify the controller pods are running by checking the flux-system namespace:
kubectl get pods -n flux-system
You should see four pods, all showing 1/1 in the READY column and Running in the STATUS column.
What do these controllers do?
- source-controller fetches manifests from Git repositories and Helm chart registries.
- kustomize-controller applies Kubernetes manifests (with optional Kustomize overlays) to the cluster.
- helm-controller manages Helm chart releases declaratively.
- notification-controller handles alerts and event forwarding.
Controllers not showing as Running?
- If pods show ContainerCreating, wait 30 seconds and run the command again. The images may still be pulling.
- Make sure Docker Desktop is running. The kind cluster depends on Docker to function.
Help me troubleshoot Flux controllers not starting
Clone the fleet-infra repository locally
The bootstrap command created the fleet-infra repository on your GitHub account and populated it with Flux's configuration files. To make future changes through Git, you need a local copy.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the fleet-infra repository and move into it by running these commands:
git clone https://github.com/$GITHUB_USER/fleet-infra
cd fleet-infra
- Confirm the directory structure exists by running:
ls clusters/my-cluster/flux-system/
You should see three files: gotk-components.yaml, gotk-sync.yaml, and kustomization.yaml.
What are these files?
- gotk-components.yaml contains all the Flux controller manifests (CRDs and deployments).
- gotk-sync.yaml contains the GitRepository and Kustomization resources that tell Flux to watch this repo.
- kustomization.yaml is a standard Kustomize file that references the other two.
These files are the heart of your GitOps setup. Flux applies them to keep itself running and synchronized with the repo.
Don't see the three files?
- Make sure you're inside the fleet-infra directory. Run pwd to confirm your current location.
- Check that the clone succeeded. If you see a permission error, your GITHUB_TOKEN may have expired. Generate a new one and export it again.
Help me troubleshoot cloning the fleet-infra repository
Your cluster now has a fully operational GitOps control plane. Next up, you'll deploy your first application entirely through a Git commit.
Deploy an Application via GitOps
Your cluster now has Flux controllers watching the fleet-infra repository for changes. The next question is: how do you actually deploy an application without running kubectl apply?
In GitOps, you declare what should run on the cluster inside a Git repo, and Flux pulls that declaration down and applies it automatically. In this step, you will tell Flux about a sample application called podinfo and watch it deploy without touching the cluster directly.
In this step, get ready to:
- Create a GitRepository source that points Flux to the podinfo application code.
- Create a Kustomization resource that tells Flux how to deploy that code.
- Push both manifests to GitHub and watch Flux automatically deploy the app.
Create a GitRepository source pointing to podinfo
Flux needs to know where to find application code. A GitRepository resource tells Flux the URL, branch, and polling interval for a Git repository it should track.
- Generate the GitRepository manifest by running this command:
flux create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--branch=master \
--interval=1m \
--export > ./clusters/my-cluster/podinfo-source.yaml
What does this command do?
The flux create source git command builds a GitRepository custom resource. The --export flag outputs the YAML to stdout instead of applying it to the cluster, and the > redirect saves it to a file.
Flux's source-controller will poll this repository every 1 minute to detect new commits.
- Confirm the file was created by running:
cat ./clusters/my-cluster/podinfo-source.yaml
You should see YAML output with kind: GitRepository and the podinfo URL.
Don't see the file?
Make sure you are in the fleet-infra directory. Run pwd to check your current location. If you are somewhere else, run cd ~/Desktop/fleet-infra to return.
Help me troubleshoot why my podinfo-source.yaml file was not created.
Create a Kustomization to deploy podinfo
A Kustomize resource in Flux is different from a regular Kustomize file. It tells Flux which source to pull from, which path inside that source contains the manifests, and which namespace to deploy into.
- Generate the Kustomization manifest by running this command:
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
What do these flags mean?
- --source=podinfo links this Kustomization to the GitRepository you just created.
- --path="./kustomize" tells Flux which directory inside the podinfo repo contains Kubernetes manifests.
- --prune=true means Flux will delete resources from the cluster if you remove them from Git.
- --interval=30m sets how often Flux re-applies the manifests to catch any drift.
- Confirm the file was created by running:
cat ./clusters/my-cluster/podinfo-kustomization.yaml
You should see YAML output with kind: Kustomization and a sourceRef pointing to the podinfo GitRepository.
Seeing an error about the source?
If the command fails, double-check that you typed --source=podinfo exactly. This name must match the GitRepository resource name from the previous command.
Help me fix my flux create kustomization command.
✔️ Awesome, I've got everything!
Great. Double check you have both files saved in clusters/my-cluster/ before moving on to the commit step.
ⓧ I'd like to double check the full code
Here is what podinfo-source.yaml should look like:
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 1m
ref:
branch: master
url: https://github.com/stefanprodan/podinfo
Here is what podinfo-kustomization.yaml should look like:
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
Commit and push both manifests to trigger deployment
The manifests only exist on your local machine right now. Flux watches the remote fleet-infra repository on GitHub. Once you push, Flux detects the new files and applies them to the cluster automatically.
- Stage and commit both manifest files by running:
git add -A && git commit -m "Add podinfo GitRepository and Kustomization"
- Push the commit to GitHub by running:
git push
What happens after you push?
Flux's source-controller detects the new commit in fleet-infra. It downloads the updated files and passes them to the kustomize-controller, which reads your new Kustomization resource and applies the podinfo manifests to the cluster.
This is the core GitOps loop: you commit to Git, and the cluster converges to match.
- Watch Flux reconcile in real time by running:
flux get kustomizations --watch
Wait until the podinfo kustomization shows Ready status. Press Ctrl+C to stop watching.
- Verify the podinfo application is running by checking deployments and services:
kubectl -n default get deployments,services
You should see the podinfo deployment with 2/2 READY pods and a ClusterIP service on ports 9898/TCP and 9999/TCP.
Kustomization stuck or not appearing?
If flux get kustomizations does not show a podinfo entry, wait 1-2 minutes for the source-controller to pick up your push. You can manually trigger a reconciliation by running:
flux reconcile source git flux-system
If the status shows an error about the source not being ready, confirm your podinfo-source.yaml was pushed correctly by checking the file on GitHub in your fleet-infra repository.
Help me debug why Flux is not deploying podinfo.
You just deployed an application to Kubernetes without ever running kubectl apply. Next up, you will customize this deployment and test what happens when someone makes unauthorized changes directly on the cluster.
Customize and Test Drift Correction
In the last step, you deployed podinfo entirely through Git commits. Flux pulled the manifests and applied them to your cluster without a single kubectl apply.
Now you will customize that deployment and witness Flux's most powerful feature: drift correction. When someone makes an unauthorized change directly on the cluster, Flux detects the deviation and snaps the state back to match Git.
In this step, get ready to:
- Customize the podinfo deployment using in-line Kustomize patches.
- Test drift correction by manually scaling pods and watching Flux revert the change.
- Explore Flux suspend and resume commands.
Customize the deployment using Kustomize patches
The podinfo application is deployed from a repository you do not control. In-line Kustomize patches let you override specific fields in upstream manifests without forking the source repo.
Why patches instead of forking?
Forking an upstream repo means you own every future merge conflict. In-line patches keep your customizations in your own fleet-infra repo while the upstream source stays untouched.
This is the same pattern production teams use to scale third-party services or inject environment-specific config.
- Open clusters/my-cluster/podinfo-kustomization.yaml in your text editor.
- Find the line that says wait: true at the bottom of the spec block.
- Add the following patches section directly below wait: true, at the same indentation level (two spaces):
patches:
- patch: |-
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: podinfo
spec:
minReplicas: 3
target:
name: podinfo
kind: HorizontalPodAutoscaler
What does this patch do?
The patch field contains a partial YAML manifest that Flux merges into the matching resource at apply time. Here it targets the podinfo HorizontalPodAutoscaler and overrides minReplicas from 2 to 3.
The target field tells Flux which resource in the upstream kustomize directory to apply the patch to, matching by name and kind.
- Save podinfo-kustomization.yaml.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before committing.
ⓧ I'd like to double check the full code
Your full podinfo-kustomization.yaml should look exactly like this:
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
patches:
- patch: |-
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: podinfo
spec:
minReplicas: 3
target:
name: podinfo
kind: HorizontalPodAutoscaler
- Commit and push the change to GitHub by running this command:
git add -A && git commit -m "Increase podinfo minimum replicas" && git push
- Watch Flux pick up the change by running:
flux get kustomizations --watch
What should I see?
Flux polls your Git repository at the interval you configured. Within about a minute you should see the podinfo kustomization show a new revision and a Ready status. Press Ctrl+C to stop watching once you see the update.
- Verify the patch took effect by running:
kubectl get pods
You should see 3 podinfo pods in the Running state. Previously there were 2.
Still seeing only 2 pods?
Flux may not have reconciled yet. Trigger an immediate sync by running flux reconcile kustomization podinfo and then re-check with kubectl get pods.
If the kustomization shows an error, double check the indentation in your YAML file. The patches: key must be at the same level as wait: (two spaces from the left margin).
Help me debug why my Kustomize patch isn't being applied by Flux.
Test drift correction
Drift correction is what separates GitOps from a basic CI/CD push. If someone manually edits the cluster, Flux notices the state no longer matches Git and reverts it.
- Simulate an unauthorized change by manually scaling the deployment down to 1 replica:
kubectl scale deployment podinfo --replicas=1
- Confirm the cluster now has only 1 pod by running:
kubectl get pods
You should see only 1 podinfo pod running. The others are terminating or already gone.
- Trigger an immediate Flux reconciliation instead of waiting for the 30-minute interval:
flux reconcile kustomization podinfo
What is happening behind the scenes?
Flux compares the live cluster state against what Git says should exist. It sees 1 replica running but Git (through the HPA patch) says minimum 3. Flux reapplies the manifests, and the HPA scales the deployment back up.
This is the reconciliation loop in action. It continuously pulls desired state from Git and corrects any deviation.
- Verify the pods are restored by running:
kubectl get pods
You should see 3 podinfo pods back in the Running state. Flux corrected the drift automatically.
Pods not restored to 3?
The reconciliation may take a few seconds to propagate. Wait 10-15 seconds and run kubectl get pods again.
If pods remain at 1, check the kustomization status with flux get kustomizations. Look for error messages in the MESSAGE column.
Help me understand why Flux didn't revert my manual kubectl change.
Explore Flux management commands
Sometimes you need to pause Flux from reconciling while you debug or perform planned maintenance. Flux provides suspend and resume commands for this.
- Suspend Flux updates for the podinfo kustomization by running:
flux suspend kustomization podinfo
What does suspending do?
While suspended, Flux stops reconciling that resource. Manual changes to the cluster will NOT be reverted until you resume. This is useful during debugging sessions or planned migrations.
- Resume Flux updates by running:
flux resume kustomization podinfo
- View the current reconciliation status of all kustomizations by running:
flux get kustomizations
You should see the podinfo kustomization with a Ready status and the latest revision from your Git repository.
Seeing a suspended status?
If the SUSPENDED column shows True, you ran the suspend command but forgot to resume. Run flux resume kustomization podinfo to re-enable reconciliation.
Help me understand the Flux suspend and resume commands.
Secret mission
Deploy a Helm Chart with FluxCD
You've mastered Kustomization-based deployments. Now take it further by deploying a second application using Flux's Helm controller. You'll create HelmRepository and HelmRelease resources to manage a Helm chart entirely through Git.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker containers, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- A local kind cluster named flux-demo (runs as Docker containers, no cloud costs).
- A GitHub repository named fleet-infra (free for public and private repos).
- A local clone of the fleet-infra repository on your machine.
- The kind and flux CLI tools installed via Homebrew.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your GitOps pipeline.
- The kind cluster persists until you stop Docker Desktop or explicitly delete it.
- The fleet-infra repository stays on GitHub and on your local machine.
- The kind and flux CLI tools remain installed via Homebrew.
Pause - I'll come back to this later
Shut down running processes to free up memory while keeping your files intact.
- Stop Docker Desktop. The kind cluster will automatically restart when you reopen Docker Desktop later.
- Your fleet-infra local clone and GitHub repository remain untouched.
- To resume, open Docker Desktop and wait for the kind cluster to start. Then run kubectl cluster-info --context kind-flux-demo to confirm it is healthy.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete the kind cluster by running:
kind delete cluster --name flux-demo
- Delete the local fleet-infra folder by running:
rm -rf fleet-infra
- Delete the fleet-infra repository on GitHub:
- Navigate to your fleet-infra repository on GitHub.
- Click Settings.
- Scroll down to the Danger Zone section and click Delete this repository.
- Follow the confirmation prompts to complete the deletion.
- Uninstall the CLI tools by running:
brew uninstall kind
brew uninstall fluxcd/tap/flux
- Clear the exported environment variables by closing your terminal session, or by running:
unset GITHUB_TOKEN
unset GITHUB_USER
Nice Work!
Nice Work!
You just built a complete GitOps delivery pipeline using FluxCD on a local Kubernetes cluster. Every application change flowed through Git, and Flux handled the rest.
Here's what you accomplished:
- Created a GitOps pipeline where Git is the single source of truth for cluster state, deploying the podinfo application entirely through Git commits using GitRepository and Kustomization resources.
- Customized a third-party deployment using Flux in-line Kustomize patches committed to Git, proving you can modify applications you don't control.
- Tested drift correction by manually changing cluster state and watching Flux automatically revert it to match Git.
- Secret Mission: Deployed a Helm chart declaratively using HelmRepository and HelmRelease CRDs, managing a second application entirely through Git.
Ready to quiz yourself?