
Hook: I set up the exact same app on the exact same cluster, once with ArgoCD and once with Flux. Both worked. Only one of them needed less babysitting once things went wrong.
GitOps means your cluster state lives in a Git repo, and a controller inside the cluster keeps pulling from that repo and reconciling. You stop running kubectl apply by hand. You open a pull request instead. ArgoCD and Flux are the two tools almost everyone picks for this. They solve the same problem in different ways, and the differences only show up once you actually run both.
This post walks through how each one actually works under the hood, then sets up the same small app with both of them, side by side, with the real commands and manifests. By the end you'll have a clear way to decide which one fits your team instead of picking whichever one has more GitHub stars.
The short version
- Pick ArgoCD if you want a UI your whole team (including non-Kubernetes people) can look at, you need strong multi-tenancy with "Projects," or you're already running Argo Rollouts for canary deploys.
- Pick Flux if you want something lighter that feels like a native Kubernetes extension (everything is a CRD, there's no separate UI to maintain), you want tight native support for Kustomize and Helm in one pipeline, or you want built-in SOPS secret decryption without a side controller.
- Both reconcile from Git on an interval (and on webhook), both self-heal drift, and both are CNCF graduated/incubating projects that aren't going away.
How they actually work
Both tools run a controller inside your cluster. The controller watches a Git repo and applies what it finds there. The difference is in how that's modeled.
ArgoCD is built around one core object: the Application. Each Application points at a Git repo path and a destination cluster/namespace. A separate API server and UI sit in front of that, which is why ArgoCD needs its own ingress, its own RBAC model, and its own login.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/yourorg/gitops-demo.git
targetRevision: main
path: apps/guestbook
destination:
server: https://kubernetes.default.svc
namespace: guestbook
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Flux splits the same job into smaller composable CRDs: a GitRepository (where to pull from), a Kustomization or HelmRelease (what to apply from that source), and optionally an ImageUpdateAutomation if you want it to bump image tags for you. There's no separate UI by default — you talk to it through kubectl and the flux CLI.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: gitops-demo
namespace: flux-system
spec:
interval: 1m
url: https://github.com/yourorg/gitops-demo.git
ref:
branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: guestbook
namespace: flux-system
spec:
interval: 5m
path: "./apps/guestbook"
prune: true
sourceRef:
kind: GitRepository
name: gitops-demo
targetNamespace: guestbook
That one structural choice (one object vs. several small ones) explains most of the downstream differences in this post: ArgoCD's UI has something concrete to render (an Application), while Flux's model composes more naturally if you already think in terms of Kustomize overlays.
Hands-on: the same app, deployed both ways
Let's deploy a tiny nginx-based "guestbook" app to a local kind cluster, once per tool, so you can see the actual commands and what they produce.
kind create cluster --name gitops-demo
kubectl cluster-info --context kind-gitops-demo
Repo layout for both (same app, same files — only the controller differs):
gitops-demo/
└── apps/
└── guestbook/
├── deployment.yaml
├── service.yaml
└── kustomization.yaml
# apps/guestbook/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: guestbook
spec:
replicas: 2
selector:
matchLabels:
app: guestbook
template:
metadata:
labels:
app: guestbook
spec:
containers:
- name: guestbook
image: nginx:1.27-alpine
ports:
- containerPort: 80
# apps/guestbook/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
Setting up ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl -n argocd wait --for=condition=available --timeout=180s deployment/argocd-server
# Grab the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
# Port-forward the UI/API locally
kubectl -n argocd port-forward svc/argocd-server 8080:443 &
argocd login localhost:8080 --username admin --password <PASTE> --insecure
Now apply the Application manifest from earlier and watch it sync:
kubectl apply -f application.yaml
argocd app sync guestbook
argocd app get guestbook
Illustrative output (actual columns vary by ArgoCD version):
Name: argocd/guestbook
Project: default
Server: https://kubernetes.default.svc
Namespace: guestbook
Sync Status: Synced to main
Health Status: Healthy
KIND NAME STATUS HEALTH
Deployment guestbook Synced Healthy
Service guestbook Synced Healthy
Setting up Flux
# Install the flux CLI, then bootstrap straight into your GitHub repo —
# this both installs the controllers AND commits the manifests that
# define them, so the controllers manage their own install going forward.
curl -s https://fluxcd.io/install.sh | sudo bash
export GITHUB_TOKEN=<your PAT with repo scope>
flux bootstrap github \
--owner=yourorg \
--repository=gitops-demo \
--branch=main \
--path=clusters/kind-gitops-demo \
--personal
Then point Flux at the app path with the GitRepository and Kustomization from earlier:
kubectl apply -f gitrepository.yaml
kubectl apply -f kustomization.yaml
flux get sources git
flux get kustomizations
Illustrative output (actual columns vary by Flux version):
NAME REVISION SUSPENDED READY MESSAGE
gitops-demo main@sha1:a1b2 False True stored artifact for revision 'main@sha1:a1b2'
NAME REVISION SUSPENDED READY MESSAGE
guestbook main@sha1:a1b2 False True Applied revision: main@sha1:a1b2
In both cases, delete the Deployment by hand with kubectl delete deployment guestbook -n guestbook and wait one reconcile interval — both tools will recreate it without you touching Git. That self-healing is the actual point of GitOps, and it's identical in both tools. The differences show up elsewhere.
Where they genuinely differ
Multi-tenancy and RBAC
ArgoCD has AppProject objects: you can restrict which repos, which clusters, and which resource kinds a team's Applications are allowed to touch, and layer ArgoCD's own RBAC (policy.csv) on top for who can click sync in the UI. This is built for "50 teams sharing one ArgoCD install" from day one.
Flux leans on Kubernetes-native RBAC and namespace boundaries instead — a Kustomization's serviceAccountName controls what it's allowed to apply, scoped by a normal Role/RoleBinding. It's less custom to learn, but you're assembling tenancy yourself out of standard Kubernetes primitives rather than getting it as a dedicated feature.
Secrets
Flux has first-class, built-in support for decrypting SOPS-encrypted secrets as part of the Kustomization reconcile — no extra controller. ArgoCD doesn't do this natively; the common pattern is pairing it with the separate argocd-vault-plugin or External Secrets Operator. If SOPS-in-Git is your model, Flux has less moving parts for this specific piece.
UI and visibility
ArgoCD's UI is a real selling point for mixed teams — a dependency graph of every resource an Application owns, one click to see why something's out of sync, and it's often the first thing a new engineer opens to understand what's actually running. Flux has no bundled UI; you either live in the CLI (flux get, flux logs) or bring in the separate, optional Weave GitOps dashboard.
Progressive delivery
If you want canary or blue/green rollouts, ArgoCD pairs naturally with Argo Rollouts (same project family, same UI shows the rollout progressing). Flux pairs with Flagger for the same job. Functionally comparable; the practical tiebreaker is usually "which one do you already have Prometheus metrics wired into."
A decision checklist
- Need a UI non-Kubernetes stakeholders can open? → ArgoCD
- Already deep in Kustomize/Helm and want the GitOps layer to feel like "just more Kubernetes"? → Flux
- Running many teams/tenants through one shared install? → ArgoCD (AppProjects do real work here)
- Secrets already in SOPS and want one less controller? → Flux
- Already using Argo Rollouts or Argo Workflows? → ArgoCD, for the shared ecosystem
- Want the absolute smallest footprint on the cluster? → Flux, typically fewer and lighter pods than a full ArgoCD install
Neither one is "wrong," and plenty of teams run one for years and never look back — the real mistake is picking based on a blog post's opinion instead of your own team's shape. Install both on a throwaway kind cluster like we just did, deploy something you actually run in production (not just the demo guestbook), and see which one your team reaches for without being told to.
Try the winner from this comparison on your own cluster this week — both install in under ten minutes with the commands above, and the only real cost of trying is a kind delete cluster when you're done.

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


