Skip to main content
GitOps with Argo CD: Best Practices for 2026

GitOps with Argo CD: Best Practices for 2026

KU
• 7 min read•Updated •98 views

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.

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:

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

yaml.yaml
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 ExternalSecret that references a key; the operator materializes the Secret. 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

React
Share this post
KU

Kiril Urbonas

AI Engineer

553 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.