}

Argo CD GitOps Tutorial 2026: Deploy Kubernetes Apps from Git

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 learnTime needed
Install Argo CD via kubectl and Helm15 min
Access UI and CLI10 min
Deploy your first app and Helm chart20 min
App-of-apps and ApplicationSets20 min
RBAC, SSO, multi-cluster, notifications30 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 Application and AppProject CRDs 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

  1. Navigate to https://localhost:8080 and click + New App.
  2. Fill in the Application Name, Project (use default to start), and Sync Policy.
  3. Under Source, enter your repository URL, revision (HEAD or a branch name), and the path within the repo containing your Kubernetes manifests.
  4. Under Destination, select the cluster (https://kubernetes.default.svc for in-cluster) and the target namespace.
  5. Click Create. The app appears with an OutOfSync status 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 → SettingsDeveloper settingsOAuth AppsNew 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:

  1. Sealed Secrets — Encrypt secrets with a cluster-specific key; the encrypted YAML is safe to commit.
  2. External Secrets Operator — Sync secrets from AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, etc.
  3. 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

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro