FluxCD in Production: GitOps for Kubernetes Without the ArgoCD UI
Flux is a pull-based GitOps operator that reconciles your cluster state to Git continuously — no UI, CLI-first, and built around composable controllers. Here's how Kustomization, HelmRelease, and ImageAutomation work together in a real platform setup.

Flux and ArgoCD solve the same problem: keep your Kubernetes cluster continuously reconciled to a Git repository. The difference is mostly philosophy and interface. ArgoCD has a rich web UI, an App of Apps pattern, and sync waves driven by annotations. Flux has no built-in UI, is fully CLI-driven, and composes reconciliation through native Kubernetes CRDs that stack on each other.
Neither is objectively better. ArgoCD is easier to demo and visualize. Flux is easier to automate, easier to bootstrap into a new cluster programmatically, and maps more directly to how Kubernetes itself thinks about reconciliation. If your team's instinct is to add another tool to the dashboard, choose ArgoCD. If your instinct is to treat the GitOps operator as infrastructure that runs silently in the background, Flux fits better.
This post covers how Flux actually works in production — not the getting started guide, but the patterns you need for a multi-team platform.
The Flux Controller Model
Flux decomposes GitOps into separate controllers, each with a single responsibility:
| Controller | Responsibility |
|---|---|
source-controller | Fetches and caches Git repos, Helm repos, OCI artifacts, S3 buckets |
kustomize-controller | Applies Kustomization resources to the cluster |
helm-controller | Manages HelmRelease lifecycle: install, upgrade, rollback |
notification-controller | Sends alerts to Slack, PagerDuty, webhooks; receives webhooks to trigger reconciliation |
image-reflector-controller | Scans container registries for new image tags |
image-automation-controller | Commits updated image tags back to Git |
The last two are optional and installed separately. The first four are the core.
Bootstrap
1# Install the Flux CLI
2curl -s https://fluxcd.io/install.sh | sudo bash
3
4# Bootstrap Flux into a cluster with a GitHub repository
5flux bootstrap github \
6 --owner=your-org \
7 --repository=fleet-infra \
8 --branch=main \
9 --path=clusters/my-cluster \
10 --personalBootstrap does three things: creates the fleet-infra repository if it doesn't exist, commits the Flux component manifests to clusters/my-cluster/flux-system/, and installs those manifests into the cluster. The cluster then immediately reconciles itself from the repository — Flux is self-hosting from the moment bootstrap completes.
After bootstrap, clusters/my-cluster/flux-system/ contains:
gotk-components.yaml # Flux controllers and CRDs
gotk-sync.yaml # GitRepository + Kustomization pointing at this path
kustomization.yaml # Kustomize overlay
The gotk-sync.yaml is what makes Flux self-managing: it defines a Kustomization that points at the cluster directory, so when you commit changes to Flux's own configuration, it reconciles itself.
Sources
Everything in Flux starts with a Source — a resource that fetches content and makes it available to other controllers.
GitRepository
1apiVersion: source.toolkit.fluxcd.io/v1
2kind: GitRepository
3metadata:
4 name: my-app
5 namespace: flux-system
6spec:
7 interval: 1m
8 url: https://github.com/your-org/my-app
9 ref:
10 branch: main
11 secretRef:
12 name: my-app-auth # SSH key or GitHub tokeninterval: 1m means Flux fetches the repository every minute. For faster feedback, use webhook receivers (covered below) to trigger immediate reconciliation on push.
HelmRepository
1apiVersion: source.toolkit.fluxcd.io/v1
2kind: HelmRepository
3metadata:
4 name: bitnami
5 namespace: flux-system
6spec:
7 interval: 12h
8 type: oci
9 url: oci://registry-1.docker.io/bitnamichartsThe https://charts.bitnami.com/bitnami URL was deprecated in November 2024. All Bitnami charts are now served via OCI from registry-1.docker.io/bitnamicharts. Public OCI repos need no secretRef. Private registries (ECR, GHCR) do:
1apiVersion: source.toolkit.fluxcd.io/v1
2kind: HelmRepository
3metadata:
4 name: my-charts
5 namespace: flux-system
6spec:
7 interval: 5m
8 type: oci
9 url: oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts
10 secretRef:
11 name: ecr-credentialsKustomization
Kustomization is Flux's core reconciliation resource. It takes a source reference and a path, runs kustomize build, and applies the output.
1apiVersion: kustomize.toolkit.fluxcd.io/v1
2kind: Kustomization
3metadata:
4 name: my-app
5 namespace: flux-system
6spec:
7 interval: 5m
8 path: ./deploy/production
9 prune: true
10 sourceRef:
11 kind: GitRepository
12 name: my-app
13 targetNamespace: production
14 healthChecks:
15 - apiVersion: apps/v1
16 kind: Deployment
17 name: my-app
18 namespace: production
19 timeout: 3m
20 retryInterval: 30sprune: true is the critical setting: resources removed from Git are deleted from the cluster. This is what makes it true GitOps — Git is the only source of truth. Without prune, deleted manifests leave orphaned resources.
healthChecks tells Flux to wait for those resources to reach a healthy state before marking the Kustomization as ready. If the health check fails within timeout, the Kustomization reports a failed status and retries on the next interval.
Dependency Ordering
spec:
dependsOn:
- name: infrastructure
namespace: flux-systemThe dependsOn field makes a Kustomization wait for another to be ready before applying. Use this to ensure CRDs are applied before operators that use them, or to ensure a database is running before the application that connects to it.
A typical dependency chain:
infrastructure (namespaces, CRDs, operators)
└── platform (monitoring, secrets management, ingress)
└── applications (your services)
HelmRelease
HelmRelease manages a Helm chart lifecycle — install, upgrade, test, rollback. The helm-controller handles the full Helm release state machine.
1apiVersion: helm.toolkit.fluxcd.io/v2
2kind: HelmRelease
3metadata:
4 name: payment-service
5 namespace: production
6spec:
7 interval: 10m
8 chart:
9 spec:
10 chart: payment-service
11 version: ">=2.0.0 <3.0.0"
12 sourceRef:
13 kind: HelmRepository
14 name: my-charts
15 namespace: flux-system
16 interval: 5m
17 values:
18 replicaCount: 4
19 image:
20 repository: 123456789.dkr.ecr.us-east-1.amazonaws.com/payment-service
21 tag: "2.3.1"
22 resources:
23 requests:
24 cpu: 250m
25 memory: 512Mi
26 upgrade:
27 remediation:
28 retries: 3
29 remediateLastFailure: true
30 rollback:
31 cleanupOnFail: true
32 test:
33 enable: trueupgrade.remediation.retries: 3 means if an upgrade fails, Flux retries 3 times before marking it failed. remediateLastFailure: true rolls back to the last successful release after exhausting retries.
test.enable: true runs helm test after each successful upgrade. If the test pods fail, Flux considers the upgrade failed and triggers rollback.
Values from Secrets
Keep sensitive values out of Git by referencing secrets:
1spec:
2 valuesFrom:
3 - kind: Secret
4 name: payment-service-db-creds
5 valuesKey: values.yaml
6 - kind: ConfigMap
7 name: payment-service-config
8 valuesKey: values.yaml
9 optional: trueThe secret contains a partial values.yaml that Flux merges with the inline values: block. Secrets are created by External Secrets Operator or Vault — Flux just reads the resulting K8s Secret. See secrets management in Kubernetes.
Post Renderers for Chart Patching
If you need to patch a third-party Helm chart without forking it:
1spec:
2 postRenderers:
3 - kustomize:
4 patches:
5 - patch: |
6 - op: add
7 path: /spec/template/spec/containers/0/env/-
8 value:
9 name: EXTRA_FLAG
10 value: "true"
11 target:
12 kind: Deployment
13 name: upstream-chart-deploymentpostRenderers applies kustomize patches to the rendered Helm output before it's applied to the cluster. This is the correct way to customize upstream charts — no forking, changes tracked in Git.
Image Update Automation
Flux can watch a container registry, detect new image tags, and commit the updated tag back to Git automatically. This completes the GitOps loop for continuous delivery without manual tag bumping.
Step 1: ImageRepository
1apiVersion: image.toolkit.fluxcd.io/v1beta2
2kind: ImageRepository
3metadata:
4 name: my-app
5 namespace: flux-system
6spec:
7 interval: 1m
8 image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app
9 secretRef:
10 name: ecr-credentialsFlux scans the ECR repository every minute for new tags.
Step 2: ImagePolicy
1apiVersion: image.toolkit.fluxcd.io/v1beta2
2kind: ImagePolicy
3metadata:
4 name: my-app
5 namespace: flux-system
6spec:
7 imageRepositoryRef:
8 name: my-app
9 policy:
10 semver:
11 range: ">=1.0.0 <2.0.0"ImagePolicy selects which tag to use from the scanned list. semver picks the latest tag matching the range. Alternatives:
alphabetical: order: asc/desc— use the last alphabetical tag (works with SHA tags sorted by timestamp prefix)numerical: order: asc/desc— for numeric build numbers
Step 3: Mark the field to update
In your Git repository, mark the image tag field with a Flux comment marker:
# In deploy/production/deployment.yaml
spec:
containers:
- name: my-app
image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:1.2.3 # {"$imagepolicy": "flux-system:my-app"}The comment # {"$imagepolicy": "flux-system:my-app"} tells image-automation-controller exactly which field to update and which policy to use.
Step 4: ImageUpdateAutomation
1apiVersion: image.toolkit.fluxcd.io/v1beta2
2kind: ImageUpdateAutomation
3metadata:
4 name: flux-system
5 namespace: flux-system
6spec:
7 interval: 30m
8 sourceRef:
9 kind: GitRepository
10 name: fleet-infra
11 git:
12 checkout:
13 ref:
14 branch: main
15 commit:
16 author:
17 email: fluxcdbot@users.noreply.github.com
18 name: fluxcdbot
19 messageTemplate: |
20 Update {{range .Updated.Images}}{{println .}}{{end}}
21 push:
22 branch: main
23 update:
24 path: ./clusters/my-cluster
25 strategy: SettersWhen a new tag matching the policy appears in ECR, Flux updates the marker field in the file, commits it to Git, and the Kustomization reconciles the change back to the cluster. The loop is complete: push a new image → ECR → Flux detects it → Git commit → cluster update.
Webhook Receivers for Instant Reconciliation
Polling every minute means up to 60 seconds of lag between a Git push and cluster reconciliation. Webhook receivers eliminate this:
1apiVersion: notification.toolkit.fluxcd.io/v1
2kind: Receiver
3metadata:
4 name: github-receiver
5 namespace: flux-system
6spec:
7 type: github
8 events:
9 - "ping"
10 - "push"
11 secretRef:
12 name: webhook-token
13 resources:
14 - apiVersion: source.toolkit.fluxcd.io/v1
15 kind: GitRepository
16 name: fleet-infra
17 namespace: flux-system# Get the receiver URL
kubectl get receiver github-receiver -n flux-system -o jsonpath='{.status.webhookPath}'
# → /hook/abc123...
# The full URL for the GitHub webhook
echo "https://flux.your-domain.com$(kubectl get receiver github-receiver -n flux-system -o jsonpath='{.status.webhookPath}')"Configure this URL in GitHub repo settings → Webhooks with content type application/json and the token from the secret. GitHub pushes trigger immediate reconciliation — no polling lag.
Notifications
1apiVersion: notification.toolkit.fluxcd.io/v1beta3
2kind: Provider
3metadata:
4 name: slack
5 namespace: flux-system
6spec:
7 type: slack
8 channel: "#platform-deploys"
9 secretRef:
10 name: slack-webhook-url
11---
12apiVersion: notification.toolkit.fluxcd.io/v1beta3
13kind: Alert
14metadata:
15 name: production-alerts
16 namespace: flux-system
17spec:
18 providerRef:
19 name: slack
20 eventSeverity: info
21 eventSources:
22 - kind: HelmRelease
23 name: "*"
24 namespace: production
25 - kind: Kustomization
26 name: "*"
27 namespace: flux-system
28 exclusionList:
29 - ".*no new images.*"eventSeverity: info captures both successful deployments and failures. Change to error if you only want failure alerts. The exclusionList regex filters out noisy events — image scan results with no new images are high-frequency and rarely actionable.
Multi-Team Tenancy
For a platform managing multiple teams, the recommended pattern is namespace isolation with per-team Kustomization resources that restrict where each team's resources can go.
1# Platform-level: platform creates tenant configs
2apiVersion: kustomize.toolkit.fluxcd.io/v1
3kind: Kustomization
4metadata:
5 name: team-a
6 namespace: flux-system
7spec:
8 interval: 5m
9 path: ./teams/team-a
10 prune: true
11 sourceRef:
12 kind: GitRepository
13 name: fleet-infra
14 serviceAccountName: team-a-reconciler # reconciles as a limited service account
15 targetNamespace: team-aThe serviceAccountName field makes the reconciliation run as a namespace-scoped service account rather than the cluster-admin Flux default. Team A's resources cannot affect Team B's namespace even if they try — the service account only has access to team-a.
Create the reconciler service account with namespace-scoped RBAC (not ClusterAdmin) — see Kubernetes RBAC in practice for the role design.
Flux vs ArgoCD: When to Choose Which
| Concern | Flux | ArgoCD |
|---|---|---|
| UI | None built-in (third-party UIs: Headlamp, Capacitor) | Full web UI |
| Bootstrap | flux bootstrap — programmatic, scriptable | Helm or manifests, then App of Apps |
| Helm support | HelmRelease with drift remediation, post-renderers | Native Helm with sync waves |
| Multi-cluster | Per-cluster controllers, centralized Git | Hub-spoke model, single ArgoCD controls all |
| Image automation | Built-in ImageAutomation controller | External: ArgoCD Image Updater (separate project) |
| Notifications | Built-in notification-controller | Built-in notifications |
| Progressive delivery | Flagger (separate project, Flux-aware) | Argo Rollouts (separate project) |
| RBAC for teams | Service account per tenant, namespace-scoped | AppProject with source/destination restrictions |
| GitOps purity | Stricter — everything is a K8s resource | Less strict — CLI operations also valid |
The deciding factor for most teams: if you want a visual interface to see deployment status and diff what's in-cluster vs what's in Git, ArgoCD. If you want GitOps that runs as background infrastructure and you're comfortable diagnosing issues via flux get and Kubernetes events, Flux.
For ArgoCD's approach to multi-cluster rollouts with controlled blast radius, see ArgoCD ApplicationSet Progressive Syncs.
Try the toolkit: Once Flux is deploying your Helm charts, generate the HelmRelease values structure for your services with the Helm Chart Starter Kit — scaffold the chart and values files Flux will reconcile.
Setting up GitOps for a multi-team platform and unsure whether Flux or ArgoCD fits better? Talk to us at Coding Protocols. We help platform teams design and implement GitOps workflows that scale past the initial cluster.
Related Topics
Found this useful? Share it.


