TL;DR
Argo CD is the industry-standard GitOps continuous delivery tool for Kubernetes. You declare the desired state of your cluster in Git, and Argo CD continuously reconciles the live state to match it. This tutorial walks through a full production-grade setup: installation, connecting Git repos, deploying Helm charts, the app-of-apps pattern, ApplicationSets, RBAC, GitHub SSO, multi-cluster support, Slack notifications, and automated image updates. All manifests and CLI commands are included.
| What you will learn | Time needed |
|---|---|
| Install Argo CD via kubectl and Helm | 15 min |
| Access UI and CLI | 10 min |
| Deploy your first app and Helm chart | 20 min |
| App-of-apps and ApplicationSets | 20 min |
| RBAC, SSO, multi-cluster, notifications | 30 min |
Prerequisites: A running Kubernetes cluster (k3s, kind, EKS, GKE, or AKS), kubectl configured, helm v3, and a Git repository you control.
What Is GitOps? Argo CD vs Flux
GitOps is an operational model where Git is the single source of truth for infrastructure and application configuration. Every change goes through a pull request, is reviewed, and is recorded in Git history. A controller running inside the cluster continuously compares the live state against the desired state in Git and reconciles any drift.
The two dominant GitOps tools for Kubernetes are Argo CD and Flux CD. Both are CNCF projects and both are production-ready. In practice, the community has gravitated toward Argo CD for several concrete reasons:
- CNCF Graduated status (2022) — Argo CD reached Graduated status ahead of Flux, signalling a higher maturity bar.
- Built-in UI — Argo CD ships a powerful web interface showing application health, sync status, and resource trees out of the box. Flux is CLI and GitOps Toolkit oriented with no first-party UI.
- Application abstraction — The
ApplicationandAppProjectCRDs give teams a clear unit of deployment that maps well to how platform teams think about tenancy. - ApplicationSets — The ApplicationSet controller (now merged into the core Argo CD project) makes it trivial to generate hundreds of Application objects from a single template, covering cluster generators, Git directory generators, and more.
- Ecosystem adoption — Argo CD is the default GitOps layer in many managed platforms (Akuity, Codefresh, Red Hat OpenShift GitOps) and is the most searched GitOps tool on Stack Overflow year over year.
Flux is not a bad choice — it has first-class OCI registry support and a tighter Kustomize integration — but Argo CD has broader enterprise adoption and a lower onboarding curve for teams new to GitOps.
Install Argo CD on Kubernetes
Option 1 — Plain kubectl (quickest path)
kubectl create namespace argocd
kubectl apply -n argocd -f \
https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
Wait for all pods to become ready:
kubectl -n argocd wait --for=condition=available deployment --all --timeout=120s
This installs the full multi-tenant setup including the API server, repo server, application controller, applicationset controller, notifications controller, and the Dex SSO server.
Option 2 — Helm (recommended for production)
The Helm chart gives you structured values, easier upgrades, and the ability to pin a specific version.
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--version 7.8.0 \
--set server.service.type=LoadBalancer \
--set configs.params."server\.insecure"=true
For a production values file (save as argocd-values.yaml):
global:
domain: argocd.example.com
server:
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls: true
configs:
params:
server.insecure: false
redis-ha:
enabled: true
controller:
replicas: 1
repoServer:
replicas: 2
applicationSet:
replicas: 2
Then install with:
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--version 7.8.0 \
-f argocd-values.yaml
Access the Argo CD UI and CLI
Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d && echo
Port-forward the UI (local access)
kubectl port-forward svc/argocd-server -n argocd 8080:443
Open https://localhost:8080 in your browser. Log in with username admin and the password retrieved above.
Install the argocd CLI
# Linux x86_64
curl -sSL -o argocd \
https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd
sudo mv argocd /usr/local/bin/
# macOS
brew install argocd
Login via CLI
argocd login localhost:8080 \
--username admin \
--password <YOUR_PASSWORD> \
--insecure
Change the default password immediately:
argocd account update-password
Connect a Git Repository
Argo CD needs read access to your Git repository. For a public repo, no credentials are needed. For a private repo, use an SSH key or a personal access token.
HTTPS with a token
argocd repo add https://github.com/your-org/your-repo.git \
--username git \
--password <GITHUB_TOKEN>
SSH key
ssh-keygen -t ed25519 -C "argocd" -f ~/.ssh/argocd_ed25519 -N ""
# Add the public key as a deploy key in your GitHub repo settings
cat ~/.ssh/argocd_ed25519.pub
argocd repo add [email protected]:your-org/your-repo.git \
--ssh-private-key-path ~/.ssh/argocd_ed25519
Verify the connection
argocd repo list
Create Your First Application
YAML manifest
Save the following as my-first-app.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-first-app
namespace: argocd
labels:
team: platform
spec:
project: default
source:
repoURL: https://github.com/your-org/your-repo.git
targetRevision: HEAD
path: k8s/my-first-app
destination:
server: https://kubernetes.default.svc
namespace: my-first-app
syncPolicy:
syncOptions:
- CreateNamespace=true
Apply it:
kubectl apply -f my-first-app.yaml
CLI alternative
argocd app create my-first-app \
--repo https://github.com/your-org/your-repo.git \
--path k8s/my-first-app \
--dest-server https://kubernetes.default.svc \
--dest-namespace my-first-app \
--sync-option CreateNamespace=true
UI walkthrough
- Navigate to
https://localhost:8080and click + New App. - Fill in the Application Name, Project (use
defaultto start), and Sync Policy. - Under Source, enter your repository URL, revision (
HEADor a branch name), and the path within the repo containing your Kubernetes manifests. - Under Destination, select the cluster (
https://kubernetes.default.svcfor in-cluster) and the target namespace. - Click Create. The app appears with an
OutOfSyncstatus until you trigger a sync.
Trigger a manual sync
argocd app sync my-first-app
Check the status:
argocd app get my-first-app
Sync Policies: Manual vs Automatic, Self-Heal, Prune
By default, Argo CD detects drift but does not act on it — a human must trigger a sync. You can change this with syncPolicy.
Automatic sync
syncPolicy:
automated:
prune: false
selfHeal: false
With automated, Argo CD syncs whenever it detects a difference between Git and the live cluster state (checked every 3 minutes by default, or immediately on a webhook push).
Self-heal
syncPolicy:
automated:
prune: false
selfHeal: true
With selfHeal: true, if someone manually edits a resource in the cluster (bypassing Git), Argo CD will revert it back to the Git state within minutes. This is the core GitOps guarantee.
Prune
syncPolicy:
automated:
prune: true
selfHeal: true
With prune: true, resources that exist in the cluster but have been removed from the Git repo are deleted on the next sync. This is powerful but potentially destructive — enable it once you are confident your Git state is complete.
Full production sync policy
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
PruneLast=true ensures new resources are healthy before old ones are removed during a rollout. The retry policy handles transient API server errors.
Deploying Helm Charts via Argo CD
Argo CD has native Helm support. You do not need to pre-render charts; Argo CD templates them at sync time, so the raw values are stored in Git and auditable.
Example: deploy cert-manager
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager
namespace: argocd
spec:
project: default
source:
repoURL: https://charts.jetstack.io
chart: cert-manager
targetRevision: v1.16.0
helm:
releaseName: cert-manager
values: |
installCRDs: true
global:
leaderElection:
namespace: cert-manager
destination:
server: https://kubernetes.default.svc
namespace: cert-manager
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Helm values from a values file in Git
If your values file lives in a different repo or path, use valueFiles:
source:
repoURL: https://github.com/your-org/helm-values.git
targetRevision: HEAD
path: cert-manager
helm:
releaseName: cert-manager
valueFiles:
- values.yaml
- values-production.yaml
Multiple sources (Argo CD v2.6+)
Argo CD supports multiple sources per Application, letting you pull the chart from one repo and the values from another:
sources:
- repoURL: https://charts.jetstack.io
chart: cert-manager
targetRevision: v1.16.0
helm:
valueFiles:
- $values/cert-manager/values-production.yaml
- repoURL: https://github.com/your-org/helm-values.git
targetRevision: HEAD
ref: values
App-of-Apps Pattern: Manage Multiple Apps Declaratively
The app-of-apps pattern uses a single "root" Argo CD Application that points to a directory of other Application manifests. This lets you bootstrap an entire cluster from a single kubectl apply.
Repository structure
gitops-repo/
├── apps/
│ ├── root-app.yaml # The root Application (bootstraps everything)
│ └── templates/
│ ├── cert-manager.yaml
│ ├── ingress-nginx.yaml
│ ├── prometheus-stack.yaml
│ └── my-service.yaml
└── k8s/
└── my-service/
├── deployment.yaml
└── service.yaml
Root Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-org/gitops-repo.git
targetRevision: HEAD
path: apps/templates
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Bootstrap the cluster with one command:
kubectl apply -f apps/root-app.yaml
Argo CD finds every Application YAML under apps/templates/, creates those Application objects in the argocd namespace, and each child application then syncs its own resources. Adding a new app to the cluster is as simple as adding a YAML file to apps/templates/ and pushing to Git.
ApplicationSets: Generate Apps from Git Directories
ApplicationSets take the app-of-apps concept further by generating Application objects dynamically from a template and a generator. The Git directory generator is particularly useful for monorepos.
Git directory generator
Suppose you have this layout:
services/
├── api-gateway/
│ └── kustomization.yaml
├── user-service/
│ └── kustomization.yaml
└── payment-service/
└── kustomization.yaml
This ApplicationSet creates one Application per directory:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: services
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: https://github.com/your-org/gitops-repo.git
revision: HEAD
directories:
- path: services/*
template:
metadata:
name: "{{.path.basename}}"
labels:
app.kubernetes.io/name: "{{.path.basename}}"
spec:
project: default
source:
repoURL: https://github.com/your-org/gitops-repo.git
targetRevision: HEAD
path: "{{.path.path}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{.path.basename}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Cluster generator (multi-cluster)
generators:
- clusters:
selector:
matchLabels:
environment: production
This generates one Application per cluster registered in Argo CD that has the environment: production label. Combined with the Git directory generator using a matrix generator, you can fan out every service to every cluster with a single ApplicationSet.
RBAC: Projects, Roles, and Policies
AppProjects
An AppProject defines what a team can deploy, to which clusters, and from which repos. It is the primary tenancy boundary in Argo CD.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-payments
namespace: argocd
spec:
description: "Payments team project"
sourceRepos:
- https://github.com/your-org/payments-*.git
destinations:
- namespace: payments-*
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceBlacklist:
- group: ""
kind: ResourceQuota
roles:
- name: developer
description: "Read-only access plus manual sync"
policies:
- p, proj:team-payments:developer, applications, get, team-payments/*, allow
- p, proj:team-payments:developer, applications, sync, team-payments/*, allow
groups:
- github-org:payments-developers
- name: deployer
description: "Full access for CI/CD"
policies:
- p, proj:team-payments:deployer, applications, *, team-payments/*, allow
Global RBAC policy
Edit the argocd-rbac-cm ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:readonly
policy.csv: |
p, role:org-admin, applications, *, */*, allow
p, role:org-admin, clusters, get, *, allow
p, role:org-admin, repositories, *, *, allow
p, role:org-admin, projects, *, *, allow
g, github-org:platform-team, role:org-admin
policy.default: role:readonly means any authenticated user who has no explicit role mapping gets read-only access — a safe default for enterprise environments.
SSO with Dex (GitHub OAuth Example)
Argo CD ships Dex as a built-in OIDC provider. Configuring GitHub OAuth takes about 5 minutes.
1. Create a GitHub OAuth App
Go to your GitHub organization → Settings → Developer settings → OAuth Apps → New OAuth App.
- Homepage URL:
https://argocd.example.com - Authorization callback URL:
https://argocd.example.com/api/dex/callback
Note the Client ID and generate a Client Secret.
2. Create the secret
kubectl -n argocd create secret generic argocd-dex-github \
--from-literal=clientID=<YOUR_CLIENT_ID> \
--from-literal=clientSecret=<YOUR_CLIENT_SECRET>
3. Configure argocd-cm
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
url: https://argocd.example.com
dex.config: |
connectors:
- type: github
id: github
name: GitHub
config:
clientID: $argocd-dex-github:clientID
clientSecret: $argocd-dex-github:clientSecret
orgs:
- name: your-github-org
teamNameField: slug
After applying this ConfigMap, Argo CD restarts Dex automatically. Users logging in with GitHub must belong to your-github-org. GitHub team slugs are used as group names in RBAC policies (e.g., your-github-org:platform-team).
Multi-Cluster Deployment with Cluster Secrets
Argo CD can manage any number of remote Kubernetes clusters. Each cluster is registered as a secret in the argocd namespace.
Register a cluster via CLI
# Ensure the target context is in your kubeconfig
kubectl config get-contexts
argocd cluster add production-eks --name production-eks
argocd cluster add staging-gke --name staging-gke
The CLI creates a ServiceAccount and ClusterRoleBinding in the target cluster and stores the credentials as a secret in the argocd namespace.
Register a cluster via secret (declarative)
apiVersion: v1
kind: Secret
metadata:
name: production-eks
namespace: argocd
labels:
argocd.argoproj.io/secret-type: cluster
environment: production
region: us-east-1
type: Opaque
stringData:
name: production-eks
server: https://ABCDEF1234567890.gr7.us-east-1.eks.amazonaws.com
config: |
{
"bearerToken": "<SERVICE_ACCOUNT_TOKEN>",
"tlsClientConfig": {
"insecure": false,
"caData": "<BASE64_CA_DATA>"
}
}
The labels on the secret are what the ApplicationSet cluster generator queries when selecting target clusters. This is how you map environment: production in an ApplicationSet to a specific set of real clusters.
List registered clusters
argocd cluster list
Notifications: Slack Alerts on Sync and Health Status
The Argo CD Notifications controller (now part of core Argo CD) sends alerts to Slack, PagerDuty, email, and more when application events occur.
1. Create the Slack bot token secret
kubectl -n argocd create secret generic argocd-notifications-secret \
--from-literal=slack-token=xoxb-<YOUR_SLACK_BOT_TOKEN>
2. Configure the notifications ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
service.slack: |
token: $slack-token
template.app-sync-succeeded: |
message: |
:white_check_mark: Application *{{.app.metadata.name}}* synced successfully.
Revision: {{.app.status.sync.revision}}
slack:
attachments: |
[{
"color": "#18be52",
"fields": [{
"title": "Sync Status",
"value": "{{.app.status.sync.status}}",
"short": true
}]
}]
template.app-sync-failed: |
message: |
:x: Application *{{.app.metadata.name}}* sync FAILED.
slack:
attachments: |
[{
"color": "#E96D76",
"fields": [{
"title": "Error",
"value": "{{.app.status.conditions[0].message}}",
"short": false
}]
}]
trigger.on-sync-succeeded: |
- when: app.status.operationState.phase in ['Succeeded']
send: [app-sync-succeeded]
trigger.on-sync-failed: |
- when: app.status.operationState.phase in ['Error', 'Failed']
send: [app-sync-failed]
trigger.on-health-degraded: |
- when: app.status.health.status == 'Degraded'
send: [app-sync-failed]
defaultTriggers: |
- on-sync-succeeded
- on-sync-failed
- on-health-degraded
3. Annotate applications to subscribe
kubectl -n argocd annotate application my-first-app \
notifications.argoproj.io/subscribe.on-sync-succeeded.slack=deployments \
notifications.argoproj.io/subscribe.on-sync-failed.slack=deployments \
notifications.argoproj.io/subscribe.on-health-degraded.slack=alerts
Or add the annotations directly to the Application manifest so they are version-controlled.
Image Updater: Automated Image Tag Updates
Argo CD Image Updater monitors a container registry and automatically updates the image tag in Git (or directly in the Application) when a new image is published.
Install Image Updater
kubectl apply -n argocd -f \
https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml
Configure registry credentials
kubectl -n argocd create secret docker-registry ghcr-credentials \
--docker-server=ghcr.io \
--docker-username=<USERNAME> \
--docker-password=<GITHUB_TOKEN>
Annotate the Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-service
namespace: argocd
annotations:
argocd-image-updater.argoproj.io/image-list: myapp=ghcr.io/your-org/my-service
argocd-image-updater.argoproj.io/myapp.update-strategy: semver
argocd-image-updater.argoproj.io/myapp.allow-tags: regexp:^v[0-9]+\.[0-9]+\.[0-9]+$
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/git-branch: main
argocd-image-updater.argoproj.io/myapp.helm.image-name: image.repository
argocd-image-updater.argoproj.io/myapp.helm.image-tag: image.tag
With write-back-method: git, the Image Updater commits the new image tag directly to the Git repository, keeping Git as the true source of record. Argo CD then detects the commit and syncs the new version to the cluster.
Update strategies: - semver — updates to the highest semantic version matching the constraint. - latest — always uses the most recently pushed tag. - name — alphabetically latest tag. - digest — tracks by image digest (useful for latest tag with digest pinning).
FAQ
Q: Can I use Argo CD with Kustomize?
Yes. If the path in your Application contains a kustomization.yaml file, Argo CD automatically uses Kustomize to render the manifests. You can pass Kustomize options like --images and --name-prefix via the kustomize block in the Application spec.
Q: How do I handle secrets in GitOps?
Never store plaintext secrets in Git. Common approaches with Argo CD:
- Sealed Secrets — Encrypt secrets with a cluster-specific key; the encrypted YAML is safe to commit.
- External Secrets Operator — Sync secrets from AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, etc.
- Vault Agent Injector — Inject secrets as files or environment variables at pod admission time.
Q: What is the difference between App health and sync status?
- Sync status (
Synced/OutOfSync) — whether the live state matches the Git state. - Health status (
Healthy/Progressing/Degraded/Missing) — whether the deployed resources are actually working (Pods running, Deployments available, etc.).
An application can be Synced but Degraded if the manifest was applied but the Pods are crash-looping.
Q: How do I roll back a bad deployment?
# List history
argocd app history my-first-app
# Roll back to a specific revision
argocd app rollback my-first-app <REVISION_NUMBER>
Note: if selfHeal is enabled, Argo CD will immediately re-sync to the current Git HEAD after a rollback. Rollbacks work best when you have already reverted the Git commit.
Q: Can I use Argo CD without internet access (air-gapped)?
Yes. Mirror the container images to an internal registry, host the Helm charts in an internal ChartMuseum or OCI registry, and point all repo URLs to your internal Git server (Gitea, GitLab self-hosted, Bitbucket Data Center). No component of Argo CD requires public internet access at runtime.
Q: How often does Argo CD poll for changes?
Every 3 minutes by default. For faster feedback, configure a webhook from your Git provider to Argo CD's /api/webhook endpoint. GitHub, GitLab, Bitbucket, and Gitea are all supported.
Q: What is the finalizers field on Application manifests?
finalizers:
- resources-finalizer.argocd.argoproj.io
When this finalizer is present, deleting an Application also cascades deletion of all the Kubernetes resources it manages. Without it, deleting the Application object leaves orphaned resources in the cluster.
Sources
- Argo CD Official Documentation — complete reference for all CRDs, CLI flags, and configuration options.
- Argo CD GitHub Repository — source code, release notes, and issue tracker.
- CNCF Argo CD Graduation Announcement — official CNCF graduation press release.
- Argo CD Helm Chart — production-grade Helm chart with full values reference.
- ApplicationSet Documentation — generators, templates, and advanced patterns.
- Argo CD Image Updater — automated image tag updates with Git write-back.
- Argo CD Notifications — full list of notification services and trigger conditions.
- GitOps Principles — the OpenGitOps working group's formal definition of GitOps principles.
- Sealed Secrets — encrypting Kubernetes secrets for safe storage in Git.
- External Secrets Operator — syncing secrets from external secret management systems into Kubernetes.