GitOps with Flux: Never Manually Apply Again

GitOps with Flux

I used to SSH into servers and run docker-compose up -d. It worked. Until I forgot what flags I used. Or I changed a config file on the server and forgot to commit it.

GitOps solves this. The Git repository is the only source of truth.

How It Works

Flux runs inside the Kubernetes cluster. It watches a Git repository. When the repo changes, Flux applies those changes to the cluster.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Dev       │────▶│   Git       │────▶│   Flux      │
│   (laptop)  │     │   Repo      │     │   (in K3s)  │
└─────────────┘     └─────────────┘     └─────────────┘


                                        ┌─────────────┐
                                        │  Cluster    │
                                        │  State      │
                                        └─────────────┘

I never run kubectl apply manually. I push to Git. Flux does the rest.

The Kustomization Structure

Flux uses Kustomize for organizing manifests. My repo structure:

infrastructure/
├── clusters/
│   └── homelab/
│       ├── flux-system/      # Flux bootstrap files
│       └── infrastructure.yaml
├── infrastructure/
│   ├── base/                 # Base definitions
│   │   ├── monitoring/
│   │   ├── ingress/
│   │   └── storage/
│   └── overlays/
│       ├── prod/             # Production patches
│       └── dev/              # Development patches
└── apps/
    ├── base/
    └── overlays/

Base definitions are generic. Overlays patch them for specific environments.

The Entrypoint

# clusters/homelab/infrastructure.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: infrastructure
  namespace: flux-system
spec:
  interval: 10m0s
  path: ./infrastructure/overlays/prod
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system

Flux checks the repo every 10 minutes. If something changed, it applies. prune: true means if I delete a manifest from Git, Flux deletes it from the cluster.

Automatic Image Updates

Flux can also watch container registries. When a new image is pushed, Flux updates the deployment and commits the change back to Git.

apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImagePolicy
metadata:
  name: app-policy
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: app-repo
  policy:
    semver:
      range: 1.x

Flow:

  1. CI builds a new image and pushes myapp:1.2.5
  2. Flux detects the new tag matching the policy
  3. Flux updates the deployment YAML to use 1.2.5
  4. Flux commits that change back to Git
  5. Flux applies the new manifest to the cluster

Fully automated. I don’t touch anything.

Handling Secrets

Secrets shouldn’t be in Git. But GitOps requires everything in Git. The solution: SOPS (Secrets OPerationS).

apiVersion: v1
kind: Secret
metadata:
  name: database-credentials
stringData:
  username: ENC[AES256_GCM,data:...,type:str]
  password: ENC[AES256_GCM,data:...,type:str]

Secrets are encrypted in Git. Flux decrypts them at apply time using a key stored in the cluster.

Disaster Recovery

This is the real power of GitOps.

If my K3s node dies:

  1. Provision new hardware
  2. Install K3s
  3. Bootstrap Flux pointing at the same repo
  4. Wait 10 minutes

The entire cluster state rebuilds automatically. Every deployment, every config, every secret — all from Git.

No runbooks. No “what was running on that server?” moments. Git remembers.

Why GitOps?

Manual ApproachGitOps
”What’s running on that server?”Check Git
”Who changed that config?”Check Git history
”Can we roll back?”git revert
”How do I rebuild this?”Bootstrap Flux
”Is prod in sync with the repo?”Flux guarantees it

Git is the source of truth. Everything else is just a cache.