
GitOps is the operational model where Git is the single source of truth for your infrastructure and application desired state, and automated controllers inside your Kubernetes cluster continuously reconcile real-world state to match Git.
While ArgoCD provides a popular centralized Web UI, Flux (v2) is built on the GitOps Toolkit (GOTK): a modular set of Kubernetes Custom Resource Definitions (CRDs) and specialized controllers that run natively in-cluster. Flux excels at multi-tenant environments, automated image updating, and minimal resource footprint.
This guide walks through setting up production-grade GitOps with Flux v2, including Kustomize reconciliation, Helm releases, SOPS secrets encryption, and automated container image PRs.
Flux breaks GitOps down into dedicated single-responsibility controllers:
Git Repository (GitHub / GitLab)
│
▼ (Pulls Git commit / tag / branch)
[ source-controller ]
│
┌──────────────────────┴──────────────────────┐
▼ (Raw manifests & Kustomize) ▼ (Helm Charts)
[ kustomize-controller ] [ helm-controller ]
│ │
▼ (Applies with Prune & Health Checks) ▼ (Executes Helm engine)
Kubernetes API Kubernetes API
▲ ▲
└──────────────────────┬──────────────────────┘
│ (Watches OCI Registry & commits new tags to Git)
[ image-automation-controller ]
source-controller: Fetches Git repos, Helm charts, OCI artifacts, and S3 buckets into cluster storage.kustomize-controller: Runs Kustomize overlays, decrypts SOPS secrets, applies manifests to the API server, and automatically prunes deleted resources.helm-controller: Declaratively manages the lifecycle of Helm chart releases.notification-controller: Emits events to Slack, Discord, or webhooks, and receives incoming webhooks from Git/OCI registries to trigger instant reconciliations.image-automation-controller: Detects new container tags in Docker registries and automatically commits updated image tags back to your Git repo.A resilient multi-environment repository layout cleanly separates base infrastructure from tenant applications:
fleet-infra/
├── clusters/
│ ├── staging/
│ │ ├── flux-system/ # Bootstrap manifests
│ │ ├── infrastructure.yaml # Reconciles /infrastructure/staging
│ │ └── apps.yaml # Reconciles /apps/staging
│ └── production/
│ ├── flux-system/
│ ├── infrastructure.yaml
│ └── apps.yaml
├── infrastructure/
│ ├── base/
│ │ ├── ingress-nginx/
│ │ └── cert-manager/
│ └── staging/
│ └── kustomization.yaml
└── apps/
├── base/
│ └── payments-api/
└── staging/
├── kustomization.yaml
└── patch-replicas.yaml
GitRepository# clusters/staging/fleet-source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: fleet-infra
namespace: flux-system
spec:
interval: 1m
url: https://github.com/my-org/fleet-infra.git
ref:
branch: main
secretRef:
name: github-deploy-token
ignore: |
# Exclude non-manifest directories from checksums
/*
!/apps
!/infrastructure
Kustomization# clusters/staging/apps.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: staging-apps
namespace: flux-system
spec:
interval: 5m
path: ./apps/staging
prune: true # Automatically deletes removed k8s resources
wait: true # Blocks until all Pods pass readiness probes
timeout: 3m
sourceRef:
kind: GitRepository
name: fleet-infra
dependsOn:
- name: staging-infrastructure # Ensures ingress/CRDs exist before apps deploy
postBuild:
substitute:
ENVIRONMENT: "staging"
CLUSTER_DOMAIN: "stage.internal.net"
Flux manages Helm charts natively without needing the Helm CLI installed locally:
# infrastructure/base/ingress-nginx/helm-release.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: ingress-nginx
namespace: flux-system
spec:
interval: 2h
url: https://kubernetes.github.io/ingress-nginx
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ingress-nginx
namespace: ingress-nginx
spec:
interval: 15m
chart:
spec:
chart: ingress-nginx
version: "4.11.x"
sourceRef:
kind: HelmRepository
name: ingress-nginx
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
driftDetection:
mode: enabled # Automatically reverts manual edits back to chart values
values:
controller:
replicaCount: 3
resources:
requests:
cpu: 100m
memory: 128Mi
Never store plaintext secrets in Git. Flux natively decrypts SOPS encrypted YAML before applying it to the cluster:
age# Generate age private key for the cluster
age-keygen -o age.agekey
# Create in-cluster secret with the private key
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey
# Encrypt local Kubernetes secret
sops --encrypt --age $(cat age.agekey | grep public | cut -d: -f2 | xargs) \
--encrypted-regex '^(data|stringData)$' \
secret.yaml > apps/staging/secret.enc.yaml
# In apps/staging/kustomization.yaml or root Kustomization CRD
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: staging-secrets
namespace: flux-system
spec:
interval: 10m
path: ./apps/staging
prune: true
sourceRef:
kind: GitRepository
name: fleet-infra
decryption:
provider: sops
secretRef:
name: sops-age # Matches private key stored in flux-system
Flux can monitor your container registry and automatically commit new image tags directly to Git:
# apps/base/payments-api/image-policy.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: payments-api
namespace: flux-system
spec:
image: ghcr.io/my-org/payments-api
interval: 1m
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: payments-api
namespace: flux-system
spec:
imageRepositoryRef:
name: payments-api
policy:
semver:
range: '^1.x' # Auto-promotes minor and patch versions
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 1m
sourceRef:
kind: GitRepository
name: fleet-infra
git:
checkout:
ref:
branch: main
commit:
author:
email: fluxcdbot@users.noreply.github.com
name: fluxcdbot
messageTemplate: 'chore(cd): update payments-api to {{range .Updated.Images}}{{println .}}{{end}}'
push:
branch: main
update:
path: ./apps/staging
strategy: Setters
In your deployment YAML, add a comment setter:
spec:
template:
spec:
containers:
- name: api
image: ghcr.io/my-org/payments-api:1.4.2 # {"$imagepolicy": "flux-system:payments-api"}
When CI pushes ghcr.io/my-org/payments-api:1.4.3, Flux detects the tag, updates this exact line in Git, commits with chore(cd)..., pushes to main, and reconciles the new Pod in staging.
| Feature | Flux v2 | ArgoCD |
|---|---|---|
| Architecture | Native Kubernetes controllers (GOTK) | Application CRD + Centralized Web Server |
| Web UI | Minimal / Weave GitOps (optional) | Rich interactive Dashboard built-in |
| Secret Decryption | Native SOPS & Age / KMS in controller | Requires ArgoCD plugin / external operator |
| Multi-Cluster | Lightweight agent pull per cluster | Central pull or push to remote clusters |
| Drift Correction | Continuous background self-healing | Continuous sync or manual sync buttons |
| Image Automation | Native in-repo auto-commit controller | Requires ArgoCD Image Updater (separate) |
A platform team is managing a multi-tenant Kubernetes cluster using Flux v2. They need to ensure that sensitive configuration data, like API keys, is stored securely in Git and decrypted only within the cluster. Which Flux component is primarily responsible for handling in-cluster decryption of encrypted Git secrets, and which standard tooling integrates with it?
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Practical guide to eliminating flat-network perimeter security in Kubernetes: default-deny NetworkPolicies, SPIFFE/SPIRE workload identity, and strict mTLS.
Read more
Beyond standard RollingUpdates: master Blue-Green and Canary deployment patterns with Argo Rollouts, Istio traffic routing, Prometheus automated analysis, and instant rollbacks.
Read more
eBPF networking in Kubernetes with Cilium: bypassing iptables/kube-proxy, direct server return (DSR), and Hubble service-mesh flow inspection.
Read more