Building a Production ArgoCD Setup: Beyond the Getting Started Guide
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:
- ArgoCD installed via Helm with HA, SSO, and RBAC.
- ApplicationSets generating Applications from git directory structure.
- Sync waves ordering dependent resources.
- Notifications closing the feedback loop to Slack/Teams/PagerDuty.
- Projects enforcing security boundaries.
- 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.