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.
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.
TipWork 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
- OpenGitOps principles - the four rules that define GitOps
- Argo CD core concepts - Application, sync, health and refresh explained
- Argo CD example apps - official sample repo to explore afterwards
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
cat > kind-config.yaml <<'EOF'kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4nodes: - role: control-plane - role: workerEOF kind create cluster --name gitops-lab --config kind-config.yamlkubectl cluster-info --context kind-gitops-labkubectl get nodesCheckpoint
NAME STATUS ROLES AGE VERSIONgitops-lab-control-plane Ready control-plane 60s v1.xx.xgitops-lab-worker Ready <none> 45s v1.xx.xBoth nodes must show Ready before you continue.
Common MistakeRunning
kubectlagainst the wrong cluster. If you have other clusters configured, runkubectl config current-contextand confirm it sayskind-gitops-labbefore every apply in this project.
Go deeper
- kind quick start - cluster creation and configuration options
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:
mkdir gitops-demo-config && cd gitops-demo-configmkdir -p apps/hello argocd cat > apps/hello/deployment.yaml <<'EOF'apiVersion: apps/v1kind: Deploymentmetadata: name: hellospec: 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: 64MiEOF cat > apps/hello/service.yaml <<'EOF'apiVersion: v1kind: Servicemetadata: name: hellospec: selector: app: hello ports: - name: http port: 9898 targetPort: httpEOF git init -b maingit add .git commit -m "feat: add hello app manifests"git remote add origin https://github.com/YOUR_USER/gitops-demo-config.gitgit push -u origin mainReplace 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.
TipThe manifests have no
namespacefield 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
- Argo CD best practices: config vs source repos - why the split matters
- podinfo on GitHub - the demo app and its endpoints
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
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=300skubectl get pods -n argocdGet the initial admin password and open the UI in a second terminal:
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:443Browse to https://localhost:8080, accept the self-signed certificate warning, and log in as admin.
Install the CLI and log in:
# macOSbrew install argocd # Linuxcurl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64chmod +x argocd && sudo mv argocd /usr/local/bin/ argocd login localhost:8080 --username admin --password 'PASTE_PASSWORD_HERE' --insecureCheckpoint
NAME READY STATUSargocd-application-controller-0 1/1 Runningargocd-applicationset-controller-... 1/1 Runningargocd-redis-... 1/1 Runningargocd-repo-server-... 1/1 Runningargocd-server-... 1/1 Runningargocd login should print 'admin:login' logged in successfully, and the UI should show an empty Applications page.
Common MistakeInstalling with plain
kubectl applyand 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-conflictsexactly as shown.
Security
--insecureskips 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
- Argo CD getting started - the official install and login guide
- Argo CD releases - pin a version tag instead of
stablefor repeatable installs
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:
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: hello namespace: argocdspec: 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=trueApply it and look at the status:
kubectl apply -f argocd/hello-app.yamlargocd app get helloThe app shows OutOfSync and Missing because nothing is deployed yet. Sync it by hand:
argocd app sync helloargocd app wait hello --healthkubectl get pods -n demoNow turn on automation. Add this block under syncPolicy in the same file, then re-apply:
automated: prune: true selfHeal: truegit add argocd/hello-app.yamlgit commit -m "feat: enable automated sync"git pushkubectl apply -f argocd/hello-app.yamlCheck that the app works:
kubectl port-forward svc/hello -n demo 9898:9898curl -s localhost:9898 | grep -E '"(version|hostname)"'Checkpoint
Sync Status: Synced to main (abc1234)Health Status: Healthy"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 MistakePutting
argocd/hello-app.yamlinside the watchedapps/hellopath. 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
- Automated sync policy - prune, self-heal and their limits
- Application specification - every field of the Application resource
- Declarative setup - managing Applications as YAML
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:
spec: replicas: 3 # ... containers: - name: podinfo image: ghcr.io/stefanprodan/podinfo:6.7.1git add apps/hello/deployment.yamlgit commit -m "deploy: hello 6.7.1 with 3 replicas"git pushArgoCD checks Git about every 3 minutes. To skip the wait, ask it to refresh:
argocd app get hello --refreshkubectl get pods -n demo -wCheckpoint
kubectl get deployment hello -n demokubectl port-forward svc/hello -n demo 9898:9898curl -s localhost:9898 | grep '"version"'NAME READY UP-TO-DATE AVAILABLEhello 3/3 3 3"version": "6.7.1",TipThe 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
- Git webhook configuration - instant syncs instead of polling
- Rolling updates in Kubernetes - what happens to the pods during this change
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
# Create drift: scale down by handkubectl scale deployment hello -n demo --replicas=1kubectl get deployment hello -n demo # Watch ArgoCD notice and fix itargocd app get hellokubectl get deployment hello -n demo -wNow delete a resource by hand:
kubectl delete service hello -n demokubectl get svc -n demoCheckpoint
Within seconds to a couple of minutes, the deployment returns to 3/3 and the hello Service reappears. See what ArgoCD recorded:
argocd app history helloCommon MistakeRunning an HPA (Horizontal Pod Autoscaler) alongside
selfHealon the same Deployment. The HPA changesreplicas, ArgoCD reverts it, and they fight forever. When an HPA owns replicas, removereplicasfrom the manifest or list it underignoreDifferences.
Go deeper
- Diffing customization - ignore fields another controller owns
- Automated sync policy - how self-heal timing works
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:
sed -i.bak 's|podinfo:6.7.1|podinfo:0.0.0-does-not-exist|' apps/hello/deployment.yamlrm apps/hello/deployment.yaml.bakgit add apps/hello/deployment.yamlgit commit -m "deploy: bad image tag (on purpose)"git push argocd app get hello --refreshkubectl get pods -n demoYou will see new pods in ImagePullBackOff while the old pods keep serving. Now roll back:
git revert HEAD --no-editgit push argocd app get hello --refreshkubectl get pods -n demoCheckpoint
Sync Status: SyncedHealth Status: HealthyAll hello pods are Running on 6.7.1 again, and git log --oneline shows both the bad commit and the revert.
Tip
argocd app rollbackalso exists, but it only works when automated sync is turned off, because otherwise ArgoCD would immediately sync back to Git. Prefergit revert: it keeps Git as the source of truth and leaves an audit trail.
Common MistakeFixing 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
- argocd app rollback - the CLI command and its limits
- git revert docs - how reverts create a new commit
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
# 1. ArgoCD is healthy and the app is syncedargocd app get hello# Expect: Sync Status Synced, Health Status Healthy # 2. The workload matches Gitkubectl get deployment hello -n demo# Expect: 3/3 ready # 3. Drift is corrected automaticallykubectl scale deployment hello -n demo --replicas=1sleep 30 && kubectl get deployment hello -n demo# Expect: back to 3/3 # 4. Git history shows deploy, bad release and revertgit log --onelineTroubleshooting
| 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
kind delete cluster --name gitops-labWhat 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
- Argo CD documentation home - full reference
- OpenGitOps - vendor-neutral GitOps principles