GitOps with Argo CD: Best Practices for 2026
The Argo CD 3.x patterns that held up under real traffic in 2026: repo layout, sync policies you can trust, secrets that stay out of git, and the anti-patterns we stopped repeating.
Key takeaways
Argo CD best practices for 2026 (Argo CD 3.5): keep manifests in a deploy repo separate from app code, generate Applications with ApplicationSets, turn on automated sync and selfHeal but scope prune to stateless workloads, keep secrets out of git with External Secrets Operator, give each team an AppProject, and add Argo Rollouts for canaries.
On this page
The Argo CD best practices that matter most: keep manifests in a deploy repo separate from app code, generate Applications with ApplicationSets, run automated sync with selfHeal but scope prune to stateless workloads, keep secrets out of git with External Secrets Operator, give each team an AppProject, and put Argo Rollouts in front of user-facing services.
We moved our first cluster to Argo CD in 2021 because kubectl apply from CI had stopped being honest: nobody could say what was running. As of October 2026 we run Argo CD 3.5, and what follows is what survived five years and the 3.0 upgrade.
Repo structure: app-of-apps, then ApplicationSets
One giant repo with every manifest flat in a folder worked for six services and fell apart at forty. The pattern that scaled: a root Application pointing at a directory of other Applications ("app-of-apps"). Git owns the membership, and adding a service is a PR that adds a file. A leaf Application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: payments
source:
repoURL: https://github.com/acme/deploy.git
targetRevision: main
path: apps/payments/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
App-of-apps handles one cluster well. Once the same twelve apps run across dev, staging, and four regional prod clusters, hand-written Applications become copy-paste with a typo waiting to bite. ApplicationSets fix that: one generator, many Applications.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payments
namespace: argocd
spec:
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: prod
- list:
elements:
- app: payments
- app: ledger
template:
metadata:
name: '{{.values.app}}-{{.name}}'
spec:
project: payments
source:
repoURL: https://github.com/acme/deploy.git
targetRevision: main
path: 'apps/{{.values.app}}/overlays/{{.metadata.labels.region}}'
destination:
server: '{{.server}}'
namespace: payments
syncPolicy:
automated:
prune: true
selfHeal: true
The matrix generator crosses registered clusters against a list of apps. Register a new prod cluster with the right labels and the apps show up. We run one ApplicationSet per team, because ownership follows the team. Note that Argo CD 3.0 always applies nested selectors, so recheck matrix generators when you upgrade from 2.x.
Sync policies, and where to slam the brakes
automated with selfHeal and prune is the config people copy first and regret later. Our rule: automated sync everywhere, selfHeal everywhere, and prune on for stateless workloads but gated for anything with a PersistentVolumeClaim or a CRD other controllers depend on. prune deletes objects that left git. If someone refactors a directory and a StatefulSet's name changes, prune will happily delete the old one, PVC and all. We watched it happen in staging.
For the changes that scare you, like a database migration or a CRD version bump, use sync waves and hooks instead of reaching for manual sync. Annotate resources with argocd.argoproj.io/sync-wave so the migration Job runs and completes before the Deployment rolls, and a failing PreSync hook halts the sync with a clear status instead of a half-applied cluster. Manual sync as a safety gate mostly means someone forgets to click the button while drift piles up.
Config and secrets
Kustomize overlays are our default for config: a base/ plus per-environment overlays/ carrying only what differs. Helm earns its place for third-party charts. The fuller argument is in Kustomize overlays that scale.
Secrets have one rule: plaintext never goes in git. The two approaches we run:
- External Secrets Operator when a real secret manager (Vault, AWS Secrets Manager, GCP Secret Manager) is the source of truth. Git holds an
ExternalSecretthat references a key; the operator materializes theSecret. Our default, because rotation happens in one place. - Sealed Secrets when you have no external manager. The controller holds the private key and decrypts in-cluster; rotation is more manual.
Progressive delivery
Argo CD syncs manifests; it does not make a rollout gradual. Argo Rollouts (v1.10 as of October 2026) replaces the Deployment with a Rollout that supports canary and blue-green and queries a metrics provider to promote or abort. Our pattern: 10 percent canary, an AnalysisTemplate checking Prometheus error rate and p99 latency, automatic abort. It has blocked releases at 3am with nobody watching. The Argo Rollouts progressive delivery walkthrough has the analysis templates we actually run.
Drift, RBAC, and knowing what's happening
Drift detection is Argo CD's quiet superpower. Every app is Synced, OutOfSync, or Unknown, and OutOfSync means live state diverged from git: someone ran kubectl edit, a mutating webhook added a sidecar. With selfHeal on, Argo CD reverts the drift within its reconciliation window; without it, the diff view shows exactly what changed. Since 3.0, Argo CD tracks resources by annotation instead of label by default, which stops false drift from tools that also set app.kubernetes.io/instance.
RBAC and multi-tenancy run through AppProjects. A Project scopes which repos an app can pull from, which clusters and namespaces it can deploy to, and which resource kinds are allowed. We give each team a Project, deny cluster-scoped resources by default, and grant them explicitly. Map SSO groups to Argo CD roles. In 3.x, update and delete on an Application no longer cascade to its managed resources, and the logs tab needs an explicit logs, get grant, so audit your RBAC policies on upgrade.
Observability: scrape the application controller into Prometheus and alert on apps stuck OutOfSync and on sync failures. Argo CD 3.0 removed argocd_app_sync_status and argocd_app_health_status; build alerts on the sync_status and health_status labels of argocd_app_info instead. A stuck app sits there looking fine in a list, so it needs an alert.
Anti-patterns we stopped repeating
- Deploying from the same repo as your app code. App churn drags your cluster history with it.
- Letting CI run
kubectl apply"just for this one thing." The exception erodes the model. - One enormous Application for a whole cluster. A single failed resource can wedge the sync for everything.
- Auto-sync with no progressive delivery on user-facing services. Fast propagation of a bad change is an outage.
Questions people ask
What are the most important Argo CD best practices?
Separate the deploy repo from app code, generate Applications with ApplicationSets, enable automated sync with selfHeal, scope prune away from stateful resources, keep secrets out of git, and isolate teams with AppProjects. Those six cover most of the incidents we have seen.
Should I enable prune and selfHeal in production?
Enable selfHeal everywhere; it reverts manual edits. Enable prune for stateless workloads, but gate it for anything with a PersistentVolumeClaim, because a renamed StatefulSet gets deleted along with its storage.
What changed in Argo CD 3.0 that affects these practices?
Annotation-based resource tracking became the default, fine-grained RBAC stopped inheriting update and delete to managed resources, logs access needs explicit RBAC, and the standalone sync and health status metrics were folded into argocd_app_info. Read the 2.14 to 3.0 upgrade notes before upgrading.
The call we'd make
Start with app-of-apps on one cluster, automated sync with self-heal on from day one, and prune scoped to stateless workloads. Move to ApplicationSets the moment you're copy-pasting Applications across environments, not before; at six services the matrix generator is overhead. Put secrets in External Secrets Operator, keep a deploy repo separate from app code, and give every team an AppProject with cluster-scoped resources denied. Add Argo Rollouts before you think you need it. The rest is discipline: if it's running in the cluster and it isn't in git, treat that as the bug.
Sources
How We Stopped Terraform Drift from Surprising On-Call
A real story of removing console-only changes, adding drift detection, and getting Terraform back in charge.
Best Practices: Kubernetes Cluster Upgrade Strategy
We upgraded a 60-node EKS cluster from 1.27 to 1.31 over six months. Four minor versions, one bad surprise, zero customer impact. Here's the playbook.
More from DevOps
Explore more articles in this category
GitHub Copilot CLI Sandboxing Is GA: What Actually Changed
GitHub made local sandboxing generally available for Copilot CLI on October 7, so the default now restricts the agent instead of trusting it.
The Tensorlake npm Compromise Targeted Your AI Tool Configs
A compromised npm release went after cloud credentials as expected, and local AI coding-assistant config files, which most teams never treated as secrets.
GitLab's New Rate Limits: What to Fix Before Oct 19
GitLab is capping unauthenticated API calls at 60 an hour starting October 19, and the preview windows land before most teams will have noticed.
You might have missed
Evergreen posts worth revisiting.