← Back to blog
ArgoCDKubernetesGitOps

Building a Production ArgoCD Setup: Beyond the Getting Started Guide

· 8 min read

In our previous post about GitOps, we explained the model: git as the single source of truth, a pull-based agent reconciling desired and actual state. ArgoCD is the tool most teams choose to implement that model, and for good reason — it is mature, well-maintained, and has a large community.

But the ArgoCD getting started guide gives you a single-cluster, admin-access, manually-created Application setup. That is fine for a demo. Production requires significantly more thought. This post covers the patterns we use across our production ArgoCD deployments.

Installation: Helm, Not the Raw Manifests

The official kubectl apply installation works, but for production you want the Helm chart. It gives you fine-grained control over resource requests, high availability configuration, and component-level customization.

# argocd-values.yaml

global:
  domain: argocd.example.com

redis-ha:
  enabled: true

controller:
  replicas: 2
  resources:
    requests:
      cpu: 500m
      memory: 512Mi
    limits:
      cpu: "2"
      memory: 2Gi
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true

server:
  replicas: 2
  ingress:
    enabled: true
    ingressClassName: nginx
    tls: true
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod

repoServer:
  replicas: 2
  resources:
    requests:
      cpu: 250m
      memory: 256Mi
    limits:
      cpu: "1"
      memory: 1Gi

applicationSet:
  replicas: 2

configs:
  params:
    server.insecure: false
    controller.diff.server.side: "true"
    controller.self.heal.timeout.seconds: "5"

Key points: enable Redis HA for the controller’s state cache, run at least 2 replicas of each component for availability, set explicit resource requests, and enable metrics for Prometheus scraping.

helm install argocd argo/argo-cd \
  --namespace argocd \
  --create-namespace \
  -f argocd-values.yaml
graph TD
    ROOT[Root App] --> INFRA[Infra Apps]
    ROOT --> PLAT[Platform Apps]
    ROOT --> WORK[Workload Apps]
    INFRA --> C1[cert-manager]
    INFRA --> C2[external-secrets]
    PLAT --> P1[monitoring]
    PLAT --> P2[ingress-nginx]
    WORK --> W1[staging envs]
    WORK --> W2[production envs]
    W2 --> CL1[Cluster EU]
    W2 --> CL2[Cluster US]

The App of Apps Pattern

Creating ArgoCD Applications manually through the UI or CLI does not scale. The App of Apps pattern solves this: you create a single root Application that points to a directory of Application manifests. ArgoCD manages its own Applications.

# clusters/production.yaml — the root Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: production-root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/org/gitops-repo.git
    targetRevision: main
    path: argocd/production
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

The argocd/production/ directory contains individual Application manifests:

# argocd/production/api.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: applications
  source:
    repoURL: https://github.com/org/gitops-repo.git
    targetRevision: main
    path: apps/production/api
  destination:
    server: https://kubernetes.default.svc
    namespace: api
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Now, adding a new application to the cluster is a git commit that adds a YAML file.

ApplicationSets: Scaling Beyond App of Apps

When you manage multiple clusters or multiple environments, even the App of Apps pattern produces repetitive YAML. ApplicationSets generate Applications from templates and generators.

Git Directory Generator

Automatically create an Application for every directory under a path:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: platform-components
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/org/gitops-repo.git
        revision: main
        directories:
          - path: platform/*
  template:
    metadata:
      name: "platform-{{path.basename}}"
    spec:
      project: platform
      source:
        repoURL: https://github.com/org/gitops-repo.git
        targetRevision: main
        path: "{{path}}"
      destination:
        server: https://kubernetes.default.svc
        namespace: "{{path.basename}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

Add a new directory under platform/, and ArgoCD automatically creates and syncs the corresponding Application.

Multi-Cluster with Cluster Generator

For multi-cluster setups, the cluster generator creates Applications across all registered clusters:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: monitoring
  namespace: argocd
spec:
  generators:
    - clusters:
        selector:
          matchLabels:
            monitoring: "enabled"
  template:
    metadata:
      name: "monitoring-{{name}}"
    spec:
      project: platform
      source:
        repoURL: https://github.com/org/gitops-repo.git
        targetRevision: main
        path: platform/monitoring
        helm:
          valueFiles:
            - "values-{{metadata.labels.environment}}.yaml"
      destination:
        server: "{{server}}"
        namespace: monitoring
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Register a new cluster with the monitoring: enabled label, and it automatically gets the monitoring stack deployed.

RBAC: Least Privilege Access

ArgoCD’s default admin account should be disabled after initial setup. Use RBAC policies to grant specific permissions to specific groups:

# argocd-rbac-cm ConfigMap

p, role:developers, applications, get, applications/*, allow
p, role:developers, applications, sync, applications/*, allow
p, role:developers, logs, get, applications/*, allow

p, role:platform-team, applications, *, */*, allow
p, role:platform-team, clusters, get, *, allow
p, role:platform-team, repositories, *, *, allow
p, role:platform-team, projects, *, *, allow

g, dev-team, role:developers
g, platform-team, role:platform-team

This configuration lets developers view and sync applications in the applications project but not modify cluster settings or platform components. The platform team gets full access.

SSO Integration

ArgoCD supports OIDC, SAML, and Dex-based SSO. For organizations using an identity provider (Keycloak, Okta, Azure AD, Google Workspace), SSO eliminates shared passwords and enables group-based RBAC.

# argocd-cm ConfigMap
oidc.config: |
  name: Keycloak
  issuer: https://sso.example.com/realms/engineering
  clientID: argocd
  clientSecret: $oidc.keycloak.clientSecret
  requestedScopes:
    - openid
    - profile
    - email
    - groups

The groups scope is critical — it allows ArgoCD to map identity provider groups to RBAC roles, so when someone joins the platform team in your IdP, they automatically get the right ArgoCD permissions.

Sync Waves and Hooks: Ordering Deployments

Not everything can be applied simultaneously. Namespaces must exist before deployments. CRDs must exist before custom resources. Database migrations must run before the application starts.

ArgoCD sync waves solve this:

# Wave 0: Namespace
apiVersion: v1
kind: Namespace
metadata:
  name: api
  annotations:
    argocd.argoproj.io/sync-wave: "0"

---
# Wave 1: ConfigMaps and Secrets
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
  namespace: api
  annotations:
    argocd.argoproj.io/sync-wave: "1"
data:
  DATABASE_HOST: "db.example.com"

---
# Wave 2: Database migration Job
apiVersion: batch/v1
kind: Job
metadata:
  name: api-migration
  namespace: api
  annotations:
    argocd.argoproj.io/sync-wave: "2"
    argocd.argoproj.io/hook: Sync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: registry.example.com/api:v1.5.0
          command: ["./migrate", "up"]
      restartPolicy: Never

---
# Wave 3: Application Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: api
  annotations:
    argocd.argoproj.io/sync-wave: "3"
spec:
  replicas: 3
  # ... deployment spec

ArgoCD applies resources in wave order, waiting for each wave to be healthy before proceeding. The migration Job runs as a sync hook — it executes during sync and is cleaned up before the next sync.

Helm vs. Kustomize

Both are first-class citizens in ArgoCD. Our guidance:

Use Helm when:

  • You are deploying third-party charts (nginx-ingress, cert-manager, Prometheus)
  • You need complex templating logic with conditionals and loops
  • You want to package and version your application as a reusable chart

Use Kustomize when:

  • You want to keep manifests as plain YAML and layer environment-specific patches
  • Your team prefers readability over template power
  • You want to avoid the Helm templating learning curve

In practice, most of our deployments use both: Helm for third-party tools, Kustomize for in-house applications. ArgoCD handles both natively.

# Kustomize-based application source
source:
  repoURL: https://github.com/org/gitops-repo.git
  targetRevision: main
  path: apps/production/api
  # ArgoCD auto-detects kustomization.yaml

# Helm-based application source
source:
  repoURL: https://charts.jetstack.io
  chart: cert-manager
  targetRevision: v1.14.x
  helm:
    releaseName: cert-manager
    valueFiles:
      - values-production.yaml

Notifications: Close the Feedback Loop

ArgoCD Notifications sends alerts when applications change state. Configure it to notify on sync failures, health degradation, and successful deployments:

# argocd-notifications-cm
triggers:
  - name: on-sync-failed
    condition: app.status.operationState.phase in ['Error', 'Failed']
    template: sync-failed

  - name: on-health-degraded
    condition: app.status.health.status == 'Degraded'
    template: health-degraded

templates:
  - name: sync-failed
    message: |
      Application {{.app.metadata.name}} sync failed.
      Revision: {{.app.status.operationState.operation.sync.revision}}

services:
  slack:
    token: $slack-token
    channel: deployments

Operational Tips

Pin your ArgoCD version. Do not use latest. ArgoCD releases frequently, and breaking changes happen. Upgrade deliberately.

Monitor the repo-server. The repo-server is the most resource-intensive component — it clones repos, renders manifests, and caches results. If your Application syncs are slow, the repo-server is usually the bottleneck. Give it more CPU and memory.

Use Projects. ArgoCD Projects scope which repositories an Application can pull from and which clusters/namespaces it can deploy to. Use them as security boundaries: the applications project can only deploy to application namespaces, not to kube-system.

Enable server-side diff. Set controller.diff.server.side: "true" in ArgoCD params. It produces more accurate diffs, especially for resources with server-side defaulting (which is most of them).

Putting It All Together

A production ArgoCD setup is not just the ArgoCD installation — it is the entire ecosystem:

  1. ArgoCD installed via Helm with HA, SSO, and RBAC.
  2. ApplicationSets generating Applications from git directory structure.
  3. Sync waves ordering dependent resources.
  4. Notifications closing the feedback loop to Slack/Teams/PagerDuty.
  5. Projects enforcing security boundaries.
  6. Monitoring via Prometheus ServiceMonitors and Grafana dashboards.

This builds on everything we have discussed in this blog series — from the foundational case for automation, through Terraform and Ansible for infrastructure provisioning, Kubernetes as the runtime platform, and GitOps as the operational model. ArgoCD is the final piece that ties it all together into a cohesive, auditable, self-healing system.


At robto, we build production ArgoCD platforms that scale from a single cluster to enterprise multi-cluster architectures. If you are moving beyond the getting started guide and need guidance on the patterns that work at scale, reach out.