FluxCD in Production: GitOps for Kubernetes Without the ArgoCD UI

Quick answer
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.
14 min read · Platform Engineering
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, when Bitnami moved chart distribution to OCI at registry-1.docker.io/bitnamicharts. Note that since 28 August 2025 Bitnami no longer publishes new charts or images to that location — what is already there stays pullable but is frozen, so check whether the chart you depend on is still maintained before wiring it into a reconciliation loop. 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.
Kubernetes Production Readiness Checklist
The pre-launch checks we run before calling a cluster production-ready — probes, resources, RBAC, upgrades, and backups. Plain Markdown you can commit to your repo.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
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: [email protected]
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.
See also
Frequently Asked Questions
How do I force Flux to reconcile immediately?
Use flux reconcile, optionally with --with-source so it pulls the latest commit before applying rather than reconciling the revision it already has. Without that flag you may reconcile an older commit and conclude Flux is ignoring your change.
Why is my Kustomization stuck and not applying?
Read its status conditions, which name the cause — usually a source that failed to fetch, a dependency that has not become ready, or a build error in the overlay. Flux reports the failure on the resource rather than in a central log, so flux get and describing the object is where the answer lives.
Do I need webhooks or is polling enough?
Polling is the default and adequate for most teams. Webhooks make reconciliation near-instant, which matters when deploys are frequent enough that the interval is noticeable. Treat it as an optimisation with polling as the fallback if the webhook fails.
How does multi-tenancy actually work in Flux?
Each tenant gets its own Kustomization with a service account, and Flux applies their manifests using that identity rather than its own. That means a tenant cannot exceed the RBAC granted to their service account even if their manifests ask for more, which is what makes the isolation real rather than conventional.
Official References
- Flux controllers — what each controller reconciles
- HelmRelease API reference — every field on the HelmRelease resource
- Flux image update automation — the ImagePolicy and ImageUpdateAutomation workflow
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


