Skip to main content

Deploy Your First App with ArgoCD and GitOps

Install ArgoCD on a local kind cluster, deploy an app from Git, and watch it sync, self-heal drift and roll back with git revert.

Blueprint Walkthrough

Architecture Overview & Problem Statement

This project installs ArgoCD on a local Kubernetes cluster, connects it to a Git repository of manifests, and shows how every change to your app (deploy, scale, roll back) becomes a Git commit that ArgoCD applies for you. You will run the full GitOps loop by hand: commit, sync, drift, self-heal and revert.

The problem GitOps solves

Picture an on-call engineer at a payments company like Razorpay. During an incident they run kubectl scale to add replicas. The incident ends, nobody updates the YAML, and three weeks later the next deploy silently resets the replica count and causes a second incident. There was no record of who changed what, and no single place that says what should be running.

GitOps fixes this with one rule: Git holds the desired state, and software inside the cluster keeps the live state matching it. ArgoCD is that software. It watches a Git repository and continuously compares it with the cluster. When the two differ it reports the app as OutOfSync, and it can apply the fix itself.

flowchart LR A[Developer] -->|git push| B[Config Repo] C[ArgoCD] -->|polls| B C -->|applies| D[Kubernetes] D -.->|drift| C classDef purple fill:#EDE9FE,stroke:#8B5CF6,stroke-width:2px,color:#1E293B classDef blue fill:#DBEAFE,stroke:#3B82F6,stroke-width:2px,color:#1E293B classDef green fill:#D1FAE5,stroke:#10B981,stroke-width:2px,color:#1E293B classDef orange fill:#FFEDD5,stroke:#F97316,stroke-width:2px,color:#1E293B classDef red fill:#FEE2E2,stroke:#EF4444,stroke-width:2px,color:#1E293B classDef slate fill:#F1F5F9,stroke:#475569,stroke-width:2px,color:#1E293B class A purple class B orange class C green class D blue linkStyle default stroke:#475569,stroke-width:2px

Read the diagram left to right. You never run kubectl apply against the app. You push to Git, and ArgoCD (running inside the cluster) pulls the change and applies it. If someone edits the cluster by hand, ArgoCD sees the drift and puts it back.

How this project fits the ArgoCD series

Project Level What it teaches
Deploy Your First App with ArgoCD and GitOps (this one) Beginner The core loop: sync, drift, self-heal, rollback
Build a GitOps CI/CD Pipeline with ArgoCD Intermediate Connect GitHub Actions to ArgoCD with an app repo and a config repo
Build a GitOps Delivery Platform with ArgoCD Advanced Multiple environments, ApplicationSets, promotion and canary releases

What you will build

  • A local 2-node Kubernetes cluster using kind
  • A Git repository of plain Kubernetes manifests (the config repo)
  • ArgoCD installed in the cluster with an Application that tracks that repo
  • A deploy, a drift fix and a rollback, all done through Git

What you need

Tool Check it works Install guide
Docker docker version Get Docker
kind kind version kind quick start
kubectl kubectl version --client Install kubectl
Git and a GitHub account git --version GitHub signup

You will install the ArgoCD CLI in Milestone 3. Plan for about 2 hours, and expect the first run to take longer because images download.

Tip

Work through the milestones in order and stop at each Checkpoint. If a checkpoint does not match, fix it before moving on. Every later step depends on the one before it.

Go deeper

Milestone 1: Create a Local Kubernetes Cluster

This milestone creates a 2-node Kubernetes cluster on your laptop using kind (Kubernetes in Docker). ArgoCD needs a cluster to manage, and a throwaway local one lets you break things safely.

Why a local cluster

kind runs each Kubernetes node as a Docker container, so the cluster starts in about a minute and deletes with one command. Nothing you do here costs money or touches a shared environment.

Steps

Bash
cat > kind-config.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
EOF
kind create cluster --name gitops-lab --config kind-config.yaml
kubectl cluster-info --context kind-gitops-lab
kubectl get nodes

Checkpoint

TEXT
NAME STATUS ROLES AGE VERSION
gitops-lab-control-plane Ready control-plane 60s v1.xx.x
gitops-lab-worker Ready <none> 45s v1.xx.x

Both nodes must show Ready before you continue.

Common Mistake

Running kubectl against the wrong cluster. If you have other clusters configured, run kubectl config current-context and confirm it says kind-gitops-lab before every apply in this project.

Go deeper

Milestone 2: Create the Config Repository

This milestone creates the Git repository that ArgoCD will watch. It holds only Kubernetes manifests for a small web app called podinfo, and no application source code.

Why a separate config repo

In GitOps the repo ArgoCD watches is the single source of truth for what runs in the cluster. Keeping manifests in their own repo means every deployment change shows up in one clean history, and reviewing a deploy is just reviewing a pull request. We use podinfo, a small public demo app, so you can focus on the workflow instead of building an image.

Steps

Create an empty public repository named gitops-demo-config on GitHub (no README), then run:

Bash
mkdir gitops-demo-config && cd gitops-demo-config
mkdir -p apps/hello argocd
cat > apps/hello/deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:6.7.0
ports:
- name: http
containerPort: 9898
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
EOF
cat > apps/hello/service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
selector:
app: hello
ports:
- name: http
port: 9898
targetPort: http
EOF
git init -b main
git add .
git commit -m "feat: add hello app manifests"
git remote add origin https://github.com/YOUR_USER/gitops-demo-config.git
git push -u origin main

Replace YOUR_USER with your GitHub username everywhere it appears in this project.

Checkpoint

Open the repository on GitHub. You should see apps/hello/deployment.yaml and apps/hello/service.yaml on the main branch.

Tip

The manifests have no namespace field on purpose. ArgoCD will place them in whichever namespace the Application names as its destination, which makes the same files reusable across environments later.

Go deeper

Milestone 3: Install ArgoCD and Log In

This milestone installs ArgoCD into your cluster, opens its web UI and logs in with the CLI. After this one-time setup, ArgoCD manages every deployment.

Why ArgoCD runs inside the cluster

ArgoCD runs as a few pods in the argocd namespace: an application controller that compares Git with the cluster, a repo server that reads and renders manifests, and an API server that powers the UI and CLI. Because it pulls from Git from inside, nothing outside the cluster needs cluster credentials.

Steps

Bash
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl wait --for=condition=ready pod --all -n argocd --timeout=300s
kubectl get pods -n argocd

Get the initial admin password and open the UI in a second terminal:

Bash
kubectl get secret argocd-initial-admin-secret -n argocd \
-o jsonpath='{.data.password}' | base64 -d; echo
kubectl port-forward svc/argocd-server -n argocd 8080:443

Browse to https://localhost:8080, accept the self-signed certificate warning, and log in as admin.

Install the CLI and log in:

Bash
# macOS
brew install argocd
# Linux
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/
argocd login localhost:8080 --username admin --password 'PASTE_PASSWORD_HERE' --insecure

Checkpoint

TEXT
NAME READY STATUS
argocd-application-controller-0 1/1 Running
argocd-applicationset-controller-... 1/1 Running
argocd-redis-... 1/1 Running
argocd-repo-server-... 1/1 Running
argocd-server-... 1/1 Running

argocd login should print 'admin:login' logged in successfully, and the UI should show an empty Applications page.

Common Mistake

Installing with plain kubectl apply and no --server-side. The ApplicationSet CRD is larger than the 262KB annotation limit of client-side apply, so the install fails with a "metadata.annotations: Too long" error. Use --server-side --force-conflicts exactly as shown.

Security

--insecure skips TLS verification, which is fine for a local port-forward and nowhere else. Rotate or delete the initial admin secret in any real cluster.

Go deeper

Milestone 4: Create Your First Application

This milestone creates an ArgoCD Application, the object that links one Git path to one cluster namespace. You will sync it manually first, then switch on automation.

Why start with manual sync

An Application says "take the manifests at this repo, branch and path, and make this namespace match them." Starting without automation lets you see the two states ArgoCD reports: Sync status (does the cluster match Git?) and Health status (are the resources actually working?). Once you understand them, turning on automated sync is a small step.

Steps

Create argocd/hello-app.yaml in your config repo:

YAML
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hello
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/YOUR_USER/gitops-demo-config.git
targetRevision: main
path: apps/hello
destination:
server: https://kubernetes.default.svc
namespace: demo
syncPolicy:
syncOptions:
- CreateNamespace=true

Apply it and look at the status:

Bash
kubectl apply -f argocd/hello-app.yaml
argocd app get hello

The app shows OutOfSync and Missing because nothing is deployed yet. Sync it by hand:

Bash
argocd app sync hello
argocd app wait hello --health
kubectl get pods -n demo

Now turn on automation. Add this block under syncPolicy in the same file, then re-apply:

YAML
automated:
prune: true
selfHeal: true
Bash
git add argocd/hello-app.yaml
git commit -m "feat: enable automated sync"
git push
kubectl apply -f argocd/hello-app.yaml

Check that the app works:

Bash
kubectl port-forward svc/hello -n demo 9898:9898
curl -s localhost:9898 | grep -E '"(version|hostname)"'

Checkpoint

TEXT
Sync Status: Synced to main (abc1234)
Health Status: Healthy
TEXT
"hostname": "hello-7d9f8c6b4-2kmpq",
"version": "6.7.0"

Two hello pods are Running in the demo namespace.

Setting What it does
automated ArgoCD syncs on its own when Git changes
prune: true Deletes cluster resources you removed from Git
selfHeal: true Reverts manual changes made directly in the cluster
CreateNamespace=true Creates the demo namespace if it is missing
Common Mistake

Putting argocd/hello-app.yaml inside the watched apps/hello path. ArgoCD would try to deploy the Application as part of itself. Keep Application manifests in a separate folder from the manifests they point at.

Go deeper

Milestone 5: Deploy a Change with a Git Commit

This milestone upgrades the app and scales it to 3 replicas by editing Git only. You never touch kubectl.

Why this is the whole point

In a push-based setup, a person or a pipeline runs commands against the cluster. Here, the commit is the deployment. The commit message records why, the author records who, and the diff records exactly what changed.

Steps

In apps/hello/deployment.yaml, change the replica count and image tag:

YAML
spec:
replicas: 3
# ...
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:6.7.1
Bash
git add apps/hello/deployment.yaml
git commit -m "deploy: hello 6.7.1 with 3 replicas"
git push

ArgoCD checks Git about every 3 minutes. To skip the wait, ask it to refresh:

Bash
argocd app get hello --refresh
kubectl get pods -n demo -w

Checkpoint

Bash
kubectl get deployment hello -n demo
kubectl port-forward svc/hello -n demo 9898:9898
curl -s localhost:9898 | grep '"version"'
TEXT
NAME READY UP-TO-DATE AVAILABLE
hello 3/3 3 3
"version": "6.7.1",
Tip

The 3-minute delay is the default polling interval. In real setups a GitHub webhook tells ArgoCD about a push immediately, so deploys start in seconds. See the webhook guide below.

Go deeper

Milestone 6: Break Things - Drift and Self-Heal

This milestone changes the cluster by hand to create drift, then watches ArgoCD put it back. Drift is any difference between what Git says and what is actually running.

Why drift matters

Manual edits are the number one source of "works in staging, not in production." With selfHeal: true, ArgoCD treats Git as the boss and reverts out-of-band changes, so the cluster cannot quietly diverge.

Steps

Bash
# Create drift: scale down by hand
kubectl scale deployment hello -n demo --replicas=1
kubectl get deployment hello -n demo
# Watch ArgoCD notice and fix it
argocd app get hello
kubectl get deployment hello -n demo -w

Now delete a resource by hand:

Bash
kubectl delete service hello -n demo
kubectl get svc -n demo

Checkpoint

Within seconds to a couple of minutes, the deployment returns to 3/3 and the hello Service reappears. See what ArgoCD recorded:

Bash
argocd app history hello
Common Mistake

Running an HPA (Horizontal Pod Autoscaler) alongside selfHeal on the same Deployment. The HPA changes replicas, ArgoCD reverts it, and they fight forever. When an HPA owns replicas, remove replicas from the manifest or list it under ignoreDifferences.

Go deeper

Milestone 7: Roll Back with Git Revert

This milestone ships a deliberately broken release, then rolls it back with git revert. In GitOps a rollback is just another commit.

Why rollback is a Git operation

The same mechanism that deploys also undoes a deploy, so there is no separate rollback tool to learn and the rollback shows up in history. Kubernetes also protects you: a rolling update keeps old pods running until new pods pass their readiness probe.

Steps

Point the image at a tag that does not exist:

Bash
sed -i.bak 's|podinfo:6.7.1|podinfo:0.0.0-does-not-exist|' apps/hello/deployment.yaml
rm apps/hello/deployment.yaml.bak
git add apps/hello/deployment.yaml
git commit -m "deploy: bad image tag (on purpose)"
git push
argocd app get hello --refresh
kubectl get pods -n demo

You will see new pods in ImagePullBackOff while the old pods keep serving. Now roll back:

Bash
git revert HEAD --no-edit
git push
argocd app get hello --refresh
kubectl get pods -n demo

Checkpoint

TEXT
Sync Status: Synced
Health Status: Healthy

All hello pods are Running on 6.7.1 again, and git log --oneline shows both the bad commit and the revert.

Tip

argocd app rollback also exists, but it only works when automated sync is turned off, because otherwise ArgoCD would immediately sync back to Git. Prefer git revert: it keeps Git as the source of truth and leaves an audit trail.

Common Mistake

Fixing a bad release by editing the cluster with kubectl set image. With self-heal on, ArgoCD reverts your fix back to the broken Git state. Always fix through Git.

Go deeper

Validation & Testing

This final step confirms the full GitOps loop works end to end and gives you a checklist to show your work.

Verification commands

Bash
# 1. ArgoCD is healthy and the app is synced
argocd app get hello
# Expect: Sync Status Synced, Health Status Healthy
# 2. The workload matches Git
kubectl get deployment hello -n demo
# Expect: 3/3 ready
# 3. Drift is corrected automatically
kubectl scale deployment hello -n demo --replicas=1
sleep 30 && kubectl get deployment hello -n demo
# Expect: back to 3/3
# 4. Git history shows deploy, bad release and revert
git log --oneline

Troubleshooting

Symptom Likely cause Fix
Too long annotation error on install Missing --server-side Re-run the install command from Milestone 3
App stuck Unknown or repo error Wrong repoURL or private repo Check the URL; add credentials with argocd repo add
App OutOfSync but nothing happens Automated sync not enabled Add the automated block or run argocd app sync hello
Pods ImagePullBackOff Image tag typo Fix the tag in Git and push
Cannot reach localhost:8080 Port-forward stopped Re-run the kubectl port-forward command

Clean up

Bash
kind delete cluster --name gitops-lab

What to try next

  • Build a GitOps CI/CD Pipeline with ArgoCD - automate the Git commit from a CI pipeline
  • Build a GitOps Delivery Platform with ArgoCD - multiple environments and canary releases

Go deeper

Found a mistake, an outdated step or a broken link?