𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme
Connect With Me on LinkedIn Buy Me a Coffee

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:

  1. Go to GitHub Fine-grained Tokens
  2. Click "Generate new token"
  3. Token name: flux-bootstrap-homelab
  4. Description: One-time token for Flux bootstrap - can revoke after use
  5. Set Repository access → "Only select repositories" → select homelab
  6. 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)
  7. 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:

  1. Installs Flux controllers in flux-system namespace
  2. Creates a GitRepository source pointing to your repo
  3. Creates a Kustomization to sync flux/config/
  4. Commits Flux manifests to your repo (you'll see a new commit)
  5. 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:

  1. Go to Repository Deploy Keys
  2. You should see a deploy key named flux-system-main-flux-system-./flux/config

Now revoke the bootstrap token (no longer needed):

  1. Go to GitHub Fine-grained Tokens
  2. Find flux-bootstrap-homelab and 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:

See: Flux Helm Image Updates

Resources

Footnotes

  1. Flux CD, "Flux Documentation," fluxcd.io. Accessed: Dec. 16, 2025. [Online]. Available: https://fluxcd.io/flux/

  2. Flux CD, "Get Started Guide," fluxcd.io. Accessed: Dec. 16, 2025. [Online]. Available: https://fluxcd.io/flux/get-started/

  3. Flux CD, "Bootstrap for GitHub," fluxcd.io. Accessed: Dec. 16, 2025. [Online]. Available: https://fluxcd.io/flux/installation/bootstrap/github/

Previous
Talhelper Cluster Bootstrap