Install Flux on k3s
Bootstrap Flux CD on a k3s cluster, set up GitRepository and Kustomization reconciliation, and deploy applications via HelmRelease — step by step.
Before you start
- ▸k3s cluster running with kubectl access and cluster-admin privileges
- ▸GitHub account with a personal access token (repo scope)
- ▸git installed and configured locally
- ▸flux CLI installed (version 2.3+ recommended)
Flux CD turns a Git repository into the single source of truth for your Kubernetes cluster. On k3s — the lightweight Kubernetes distribution common on bare-metal, VMs, and edge nodes — Flux works without modification, but a few k3s-specific details trip people up. This guide walks through bootstrapping Flux against a GitHub repository, creating a GitRepository + Kustomization reconciliation loop, and deploying an application via a HelmRelease.
Prerequisites and Environment
You need a running k3s cluster (single-node or multi-node), kubectl configured with cluster-admin rights, and the flux CLI installed. You also need a GitHub personal access token (PAT) with repo scope.
Install the Flux CLI
Debian/Ubuntu and Fedora/RHEL users can pull the official install script. Arch users can use the AUR.
# Debian / Ubuntu
curl -s https://fluxcd.io/install.sh | sudo bash
# Fedora / RHEL 9 / Rocky 9
curl -s https://fluxcd.io/install.sh | sudo bash
# Arch
yay -S flux-bin
Verify the CLI is available:
flux version --client
Point kubectl at k3s
k3s writes its kubeconfig to /etc/rancher/k3s/k3s.yaml. Export it for your session or copy it to ~/.kube/config.
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
Pre-flight Check
Run flux check --pre before bootstrapping. It validates Kubernetes version compatibility and required API groups.
flux check --pre
All checks should return ✔. k3s ships with all required APIs enabled by default; if you see a CRD warning it usually means your k3s version is too old (1.24 minimum, 1.28+ recommended).
Bootstrap Flux to GitHub
Bootstrap installs the Flux controllers into the flux-system namespace and commits their manifests to your Git repository. From that point on, the cluster reconciles itself from Git.
Export credentials
export GITHUB_TOKEN=ghp_yourTokenHere
export GITHUB_USER=your-github-username
Run the bootstrap command
Replace fleet-infra with your target repository name. The repository will be created if it does not exist.
flux bootstrap github \
--owner=${GITHUB_USER} \
--repository=fleet-infra \
--branch=main \
--path=clusters/my-k3s \
--personal \
--private
Flux commits the controller manifests under clusters/my-k3s/flux-system/ and then applies them. Watch the rollout:
kubectl -n flux-system get pods -w
You should see source-controller, kustomize-controller, helm-controller, and notification-controller reach Running status. Output will vary but looks like:
# Example output (abbreviated)
NAME READY STATUS RESTARTS
helm-controller-xxxxxxxxx-xxxxx 1/1 Running 0
kustomize-controller-xxxxxxxxx-xxxxx 1/1 Running 0
notification-controller-xxxxxxxxx-xxxxx 1/1 Running 0
source-controller-xxxxxxxxx-xxxxx 1/1 Running 0
k3s-specific note on storage
Flux controllers do not need persistent volumes by default. However, if you later enable image-reflector-controller or image-automation-controller, their image database requires a PVC. k3s ships with local-path as the default StorageClass, which satisfies this without extra configuration.
Create a GitRepository and Kustomization
With Flux running, you now define where your application manifests live and how they are applied. Clone your fleet-infra repo locally and work inside it.
git clone https://github.com/${GITHUB_USER}/fleet-infra
cd fleet-infra
Add a GitRepository source
Create the directory layout Flux expects and write a GitRepository manifest that points at an application repository.
mkdir -p clusters/my-k3s/apps
cat > clusters/my-k3s/apps/podinfo-source.yaml <<'EOF'
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 1m
url: https://github.com/stefanprodan/podinfo
ref:
branch: master
EOF
Add a Kustomization
A Kustomization tells Flux which path inside the GitRepository to apply and how often to reconcile.
cat > clusters/my-k3s/apps/podinfo-kustomization.yaml <<'EOF'
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 5m
path: ./kustomize
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: podinfo
EOF
prune: true means Flux deletes resources from the cluster when they are removed from Git — essential for keeping drift at zero.
Commit and push
git add clusters/
git commit -m "feat: add podinfo GitRepository and Kustomization"
git push
Flux polls every minute by default. Force an immediate reconcile with:
flux reconcile source git flux-system
Then watch the Kustomization apply:
flux get kustomizations --watch
Deploy an Application with a HelmRelease
Flux's helm-controller manages Helm charts declaratively. You define a HelmRepository (the chart source) and a HelmRelease (the release spec).
Add the HelmRepository
cat > clusters/my-k3s/apps/nginx-helmrepo.yaml <<'EOF'
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: ingress-nginx
namespace: flux-system
spec:
interval: 12h
url: https://kubernetes.github.io/ingress-nginx
EOF
Add the HelmRelease
cat > clusters/my-k3s/apps/nginx-helmrelease.yaml <<'EOF'
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ingress-nginx
namespace: ingress-nginx
spec:
interval: 1h
chart:
spec:
chart: ingress-nginx
version: ">=4.10.0 <5.0.0"
sourceRef:
kind: HelmRepository
name: ingress-nginx
namespace: flux-system
install:
createNamespace: true
values:
controller:
service:
type: LoadBalancer
EOF
k3s ships with Klipper as its built-in load-balancer, so type: LoadBalancer works without a cloud provider. On a single-node cluster the service will get an external IP equal to the node IP.
Commit and verify the HelmRelease
git add clusters/my-k3s/apps/
git commit -m "feat: add ingress-nginx HelmRelease"
git push
flux reconcile source git flux-system
flux get helmreleases -A
The READY column shows True once Helm has applied the chart. MESSAGE will read Helm install succeeded.
Verification
flux check
flux get sources git -A
flux get kustomizations -A
flux get helmreleases -A
All sources and workloads should show READY=True. Confirm the ingress controller is running:
kubectl -n ingress-nginx get pods,svc
Troubleshooting
Bootstrap fails with authentication error
Confirm your PAT has the repo scope (not just read:packages). Expired tokens are the most common cause. Regenerate and re-export GITHUB_TOKEN before retrying bootstrap.
Kustomization stuck in "not ready"
Inspect events on the specific object:
flux describe kustomization podinfo -n flux-system
A path not found error means the spec.path in your manifest does not match the actual directory layout in the source repository. A namespace not found error usually means targetNamespace doesn't exist and you need to add a namespace manifest or set spec.commonMetadata.annotations with a namespace creation hook.
HelmRelease fails with "chart not found"
Check that the HelmRepository is itself ready before the HelmRelease reconciles:
flux get sources helm -A
If the repository shows an error, the URL may be unreachable from the cluster. On k3s behind a corporate proxy, set HTTPS_PROXY in the source-controller deployment or use an internal chart mirror.
Controllers crashlooping after bootstrap
k3s runs containerd; pull image failures are the usual cause on air-gapped nodes. Pre-pull the Flux controller images with k3s ctr images pull or configure a registry mirror in /etc/rancher/k3s/registries.yaml.
Frequently asked questions
- Can I bootstrap Flux using GitLab or Gitea instead of GitHub?
- Yes. Flux supports flux bootstrap gitlab and flux bootstrap gitea with equivalent flags. For self-hosted instances add --hostname to point at your server.
- Does Flux conflict with k3s's built-in Helm controller?
- k3s ships its own HelmChart CRD for bootstrapping cluster components. Flux's helm-controller manages a separate HelmRelease CRD and the two coexist without conflict, but do not mix both approaches for the same release.
- How do I store Kubernetes Secrets in Git safely with Flux?
- Use Mozilla SOPS or Sealed Secrets. Flux's kustomize-controller has native SOPS decryption support; add a .sops.yaml and an age or GPG key reference in your Kustomization spec.
- How do I force Flux to reconcile immediately without waiting for the interval?
- Run flux reconcile source git flux-system to re-fetch Git, then flux reconcile kustomization <name> or flux reconcile helmrelease <name> to re-apply a specific resource immediately.
- What happens to my cluster if the Git repository is temporarily unreachable?
- Flux continues running the last successfully applied state. It will not delete or modify existing resources; it simply retries the source fetch until Git is reachable again.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.