Homelab
Flux CD Kubernetes GitOps
Setting Up Flux CD for GitOps: Pull-Based Kubernetes Deployments with SOPS Encryption
Overview
Bootstrapping Flux CD1 for GitOps. After this, all cluster changes happen through Git - edit YAML, commit, push, and Flux applies the changes automatically.
| Tip: | Having trouble? See v0.6.0 for reference. |
Before You Begin
Prerequisites
- Talhelper Cluster Bootstrap completed (cluster bootstrapped and healthy)
- Both nodes accessible via
kubectl - GitHub personal access token with repo permissions
- Review the Flux Get Started guide2 for background
Why This Approach
- Pull-based: Cluster pulls from Git (no credentials leaving your network)
- Self-healing: Cluster auto-reconciles to match Git state
- Reproducible: One bootstrap command restores entire cluster
- Helm native: HelmRelease CRDs wrap existing charts
How Flux Works
Here's the GitOps workflow once Flux is running:
flowchart LR
A[Edit YAML in k8s/] --> B[git commit] --> C[git push]
C --> D[Flux detects change]
D --> E[Pulls from Git]
E --> F[Applies to cluster] Flux checks for changes every minute (configurable). You can also force an immediate sync using the Flux CLI (covered after installation).
Install Flux CLI
Homebrew
brew install fluxcd/tap/flux Prepare Repository Structure
Create the directory structure Flux will manage.
Create Directories
cd ~/homelab
export KUBECONFIG=$(pwd)/talos/clusterconfig/kubeconfig
mkdir -p flux/config k8s/{core,apps} | Note: | The KUBECONFIG export only applies to your current terminal session. All kubectl and flux commands in this article assume it's set. If you open a new terminal, re-run the cd and export commands. |
Create GitHub Token
Flux needs a token to bootstrap - but only once.
Fine-grained token (recommended) - scoped to just this repo:
- Go to GitHub Fine-grained Tokens
- Click "Generate new token"
- Token name:
flux-bootstrap-homelab - Description:
One-time token for Flux bootstrap - can revoke after use - Set Repository access → "Only select repositories" → select
homelab - Under Permissions → expand "Repository permissions":
- Administration: Change dropdown to "Read and write" (creates deploy key)
- Contents: Change dropdown to "Read and write" (commits Flux manifests)
- Metadata: Leave as "Read-only" (required, can't change)
- Click "Generate token" and copy it
| Note: | Why no SOPS? This token is ephemeral. During bootstrap, Flux creates a deploy key (SSH key) and stores it as a cluster Secret. After bootstrap, Flux uses the deploy key for all Git operations - not your token. You can revoke the token immediately after bootstrap succeeds. If you ever re-bootstrap, just generate a fresh token. |
Bootstrap Flux
The bootstrap command3 installs Flux into your cluster and connects it to your Git repository.
Run Bootstrap
export GITHUB_TOKEN=<your-token>
flux bootstrap github \
--owner=<github-username> \ # e.g., lelopez-io
--repository=homelab \
--branch=main \
--path=flux/config \
--personal # using personal access token, not GitHub App What this does:
- Installs Flux controllers in
flux-systemnamespace - Creates a GitRepository source pointing to your repo
- Creates a Kustomization to sync
flux/config/ - Commits Flux manifests to your repo (you'll see a new commit)
- Creates a deploy key in your GitHub repo for ongoing Git access
Wait for completion - bootstrap takes 1-2 minutes.
Verify Deploy Key and Revoke Token
Confirm the deploy key was created:
- Go to Repository Deploy Keys
- You should see a deploy key named
flux-system-main-flux-system-./flux/config
Now revoke the bootstrap token (no longer needed):
- Go to GitHub Fine-grained Tokens
- Find
flux-bootstrap-homelaband delete it
Verify Flux Installation
Flux Pods
kubectl get pods -n flux-system Expected: All pods Running (source-controller, kustomize-controller, helm-controller, notification-controller)
Flux Health
flux check All checks should pass.
Git Sync
flux get sources git Should show flux-system GitRepository with Ready: True.
Enable SOPS Decryption
Flux can decrypt SOPS-encrypted secrets when applying them to the cluster. We'll set this up now so it's ready when we need encrypted secrets (starting with Tailscale in Tailscale Kubernetes Subnet Router).
Create Age Secret
GitOps Exception: We established that all changes go through Git. This is the one exception - the age private key that decrypts everything else cannot itself be encrypted and stored in Git. This is a bootstrap problem: you need one secret outside the system to unlock everything inside it.
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=<(op document get "sops-key | homelab") Verify Age Secret
kubectl get secret sops-age -n flux-system With this secret in place, Flux can decrypt any *.sops.yaml files when applying them to the cluster.
Create Sync Configuration
Tell Flux to watch the k8s/ directory for all resources.
Sync Kustomization
flux/config/sync.yaml:
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: sync # identifies this sync
namespace: flux-system # where Flux runs
spec:
interval: 10m # how often to check for changes
path: ./k8s # directory to sync from repo
prune: true # delete resources removed from Git
sourceRef:
kind: GitRepository
name: flux-system # use the repo Flux bootstrapped with
decryption:
provider: sops
secretRef:
name: sops-age # references the secret we created earlier Root Kustomization
Create kustomization files so Flux can discover resources.
k8s/kustomization.yaml:
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- core
- apps Core Kustomization
k8s/core/kustomization.yaml:
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []
# Added in subsequent articles:
# - tailscale (07)
# - metallb, ingress-nginx, longhorn (08)
# - gpu (09) Apps Kustomization
k8s/apps/kustomization.yaml:
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []
# Added in subsequent articles:
# - plex (10)
# - factorio (11)
# - minecraft (12) | Note: | Kustomization vs HelmRelease: A Flux Kustomization syncs a directory - it watches a path and applies everything there. A HelmRelease installs a Helm chart. We'll put HelmReleases inside k8s/core/. This Kustomization syncs them, and Flux's helm-controller installs the charts. |
Commit Sync Config
git add flux/ k8s/
git commit -m "feat(flux): init sync for k8s/core"
git push Verify Sync Configuration
# Force Flux to pick up the change immediately
flux reconcile source git flux-system
# Check the new Kustomization exists
flux get kustomizations Should show sync Kustomization with Ready: True. The empty kustomization files satisfy Flux - it will apply actual resources once we add them.
Next Steps
With Flux running, the next step is Tailscale for remote access. This is the last step requiring physical presence - after that, everything can be done remotely.
See: Tailscale Kubernetes Subnet Router
For updating deployed Helm releases and images:
Resources
Footnotes
Flux CD, "Flux Documentation," fluxcd.io. Accessed: Dec. 16, 2025. [Online]. Available: https://fluxcd.io/flux/ ↩
Flux CD, "Get Started Guide," fluxcd.io. Accessed: Dec. 16, 2025. [Online]. Available: https://fluxcd.io/flux/get-started/ ↩
Flux CD, "Bootstrap for GitHub," fluxcd.io. Accessed: Dec. 16, 2025. [Online]. Available: https://fluxcd.io/flux/installation/bootstrap/github/ ↩