Platform Engineering
14 min readMay 29, 2026

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.

AJ
Ajeet Yadav
Platform & Cloud Engineer
FluxCD in Production: GitOps for Kubernetes Without the ArgoCD UI

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:

ControllerResponsibility
source-controllerFetches and caches Git repos, Helm repos, OCI artifacts, S3 buckets
kustomize-controllerApplies Kustomization resources to the cluster
helm-controllerManages HelmRelease lifecycle: install, upgrade, rollback
notification-controllerSends alerts to Slack, PagerDuty, webhooks; receives webhooks to trigger reconciliation
image-reflector-controllerScans container registries for new image tags
image-automation-controllerCommits updated image tags back to Git

The last two are optional and installed separately. The first four are the core.


Bootstrap

bash
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  --personal

Bootstrap 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

yaml
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 token

interval: 1m means Flux fetches the repository every minute. For faster feedback, use webhook receivers (covered below) to trigger immediate reconciliation on push.

HelmRepository

yaml
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/bitnamicharts

The 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:

yaml
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-credentials

Kustomization

Kustomization is Flux's core reconciliation resource. It takes a source reference and a path, runs kustomize build, and applies the output.

yaml
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: 30s

prune: 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

yaml
spec:
  dependsOn:
    - name: infrastructure
      namespace: flux-system

The 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.

yaml
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: true

upgrade.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:

yaml
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: true

The 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:

yaml
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-deployment

postRenderers 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

yaml
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-credentials

Flux scans the ECR repository every minute for new tags.

Step 2: ImagePolicy

yaml
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:

yaml
# 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

yaml
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: Setters

When 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:

yaml
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
bash
# 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

yaml
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.

yaml
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-a

The 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

ConcernFluxArgoCD
UINone built-in (third-party UIs: Headlamp, Capacitor)Full web UI
Bootstrapflux bootstrap — programmatic, scriptableHelm or manifests, then App of Apps
Helm supportHelmRelease with drift remediation, post-renderersNative Helm with sync waves
Multi-clusterPer-cluster controllers, centralized GitHub-spoke model, single ArgoCD controls all
Image automationBuilt-in ImageAutomation controllerExternal: ArgoCD Image Updater (separate project)
NotificationsBuilt-in notification-controllerBuilt-in notifications
Progressive deliveryFlagger (separate project, Flux-aware)Argo Rollouts (separate project)
RBAC for teamsService account per tenant, namespace-scopedAppProject with source/destination restrictions
GitOps purityStricter — everything is a K8s resourceLess 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

FluxCD
GitOps
Kubernetes
HelmRelease
Kustomization
Platform Engineering
DevOps

Found this useful? Share it.

Practice this

Read Next