Argo CD and Helm · Lesson 3 of 11 · about 25 minutes

Your own Application, from your own repository

Argo CD stops deploying someone else's example and deploys the bookshop from gitops-practice. A first push puts it on the cluster in 10 seconds. A change made through a pull request takes 265 seconds to arrive, because Argo CD only checks Git on a timer. And Argo CD says Synced while the page still shows the old text, because a ConfigMap change doesn't restart anything.

By the end you'll know

  • How to point an Application at your own repository, branch and folder
  • What a deploy looks like in GitOps: a pull request, merged
  • How Argo CD finds out about a change, and why it can take minutes
  • How Argo CD's history lines up with git log
  • Why Synced doesn't always mean users see the change, with a ConfigMap as the example

Before you start

  • You need: Lessons 1 and 2 done, with the gitops cluster running and the argocd CLI logged in.
  • Your own copy of the repository: to follow along with your own commits, fork Kwamib/gitops-practice on GitHub, clone your fork, and change repoURL in argocd/bookshop.yaml to your fork's address. Argo CD can read any public repository, so no keys are needed.
  • Get the new files: git pull. You'll have apps/bookshop/ and argocd/bookshop.yaml. (If you're starting after Lesson 4, the page is already the full storefront rather than the two lines shown here. Everything else works the same.)
  • Where commands run: your own computer's terminal, in gitops-practice.

The short version

  • The app. The bookshop, as plain Kubernetes YAML in apps/bookshop/: a page in a ConfigMap, nginx to serve it, and a Service.
  • The deploy. One Application points Argo CD at that folder on main. It was running 10 seconds later.
  • A change. Two copies, and a new line on the page, through a pull request. Argo CD noticed the merge 265 seconds later, on its own routine check of GitHub.
  • The trap. Argo CD said Synced, but the page still showed the old text. A ConfigMap change doesn't restart pods; the file inside them catches up later. Lesson 6 has the fix.

From an example to your own

Lessons 1 and 2 used Argo CD's own example repository. That shows how Argo CD works, but not the part that matters: you change Git, and the cluster changes. For that, Argo CD needs to watch a repository you can commit to. This lesson uses gitops-practice, the same repository the lessons' files are in.

The bookshop, as plain YAML

apps/bookshop/ is the bookshop website, in three files you'd recognize from the Kubernetes track:

OUTPUT
apps/bookshop/configmap.yaml
apps/bookshop/deployment.yaml
apps/bookshop/service.yaml
argocd/bookshop.yaml
argocd/guestbook.yaml

The page lives in a ConfigMap:

YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: bookshop-page
data:
  index.html: |
    <h1>bookshop</h1>
    <p>Deployed by Argo CD, from gitops-practice.</p>

The Deployment runs nginx, pinned to 1.28, and mounts that ConfigMap as nginx's web folder, so index.html becomes the home page:

YAML
spec:
  replicas: 1
  ...
      containers:
        - name: nginx
          image: nginx:1.28
          ports:
            - containerPort: 80
          volumeMounts:
            - name: page
              mountPath: /usr/share/nginx/html
          readinessProbe:
            httpGet:
              path: /
              port: 80
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              memory: 64Mi
      volumes:
        - name: page
          configMap:
            name: bookshop-page

The readiness check and the resource limits are from the Kubernetes track's health-check and limits lessons. And a Service named bookshop gives it a stable address. There's no namespace in any of the files: the Application decides where they go.

The Application

YAML
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: bookshop
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Kwamib/gitops-practice.git
    targetRevision: main
    path: apps/bookshop
  destination:
    server: https://kubernetes.default.svc
    namespace: bookshop
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

The same shape as the guestbook's, with three changes: your repository, targetRevision: main rather than HEAD, so it follows one named branch, and path: apps/bookshop. Everything in that folder, and only that folder, belongs to this app.

The first deploy

The files go to GitHub first. Argo CD reads GitHub, not your laptop, so a file that isn't pushed doesn't exist as far as it's concerned:

BASH
git add apps argocd/bookshop.yaml
git commit -m "Lesson 3: the bookshop app, and its Application"
git push origin main
kubectl apply -f argocd/bookshop.yaml
OUTPUT
6d00a98 Lesson 3: the bookshop app, and its Application

application.argoproj.io/bookshop created
synced to 6d00a98 and Healthy after 10s

(The last line is the lesson's script, checking every five seconds until Argo CD reports that exact commit, and Healthy.) Ten seconds. A new Application is checked straight away.

BASH
argocd app get bookshop
kubectl get pods -n bookshop
OUTPUT
Sync Status:        Synced to main (6d00a98)
Health Status:      Healthy

GROUP  KIND        NAMESPACE  NAME           STATUS  HEALTH   HOOK  MESSAGE
       ConfigMap   bookshop   bookshop-page  Synced                 configmap/bookshop-page unchanged
       Service     bookshop   bookshop       Synced  Healthy        service/bookshop unchanged
apps   Deployment  bookshop   bookshop       Synced  Healthy        deployment.apps/bookshop unchanged

NAME                        READY   STATUS    RESTARTS   AGE
bookshop-8467b4689d-mkm6t   1/1     Running   0          10s

Synced to the commit you just pushed, all three resources there. (The ConfigMap has no health: there's nothing about a ConfigMap that can be unhealthy.) Is it serving? A port-forward to the Service, in its own terminal, then curl:

BASH
kubectl port-forward svc/bookshop -n bookshop 8081:80
curl -s http://localhost:8081/
OUTPUT
<h1>bookshop</h1>
<p>Deployed by Argo CD, from gitops-practice.</p>

The page from the ConfigMap, served by nginx, deployed from your repository.

A change, the GitOps way

Now a real change: two copies of the bookshop, and a line on the page saying so. Not with kubectl. On a branch, through a pull request, the way a team would:

BASH
git switch -c two-copies
# edit replicas: 1 to replicas: 2, and the line on the page
git diff
OUTPUT
--- a/apps/bookshop/configmap.yaml
+++ b/apps/bookshop/configmap.yaml
@@ -6,4 +6,4 @@ metadata:
 data:
   index.html: |
     <h1>bookshop</h1>
-    <p>Deployed by Argo CD, from gitops-practice.</p>
+    <p>Deployed by Argo CD, from gitops-practice. Now with two copies.</p>
--- a/apps/bookshop/deployment.yaml
+++ b/apps/bookshop/deployment.yaml
@@ -6,7 +6,7 @@ metadata:
   labels:
     app: bookshop
 spec:
-  replicas: 1
+  replicas: 2
   selector:
     matchLabels:
       app: bookshop
BASH
git commit -am "Bookshop: two copies, and say so on the page"
git push -u origin two-copies
gh pr create --fill --base main
gh pr merge --squash --delete-branch
git switch main && git pull
git log --oneline -3
OUTPUT
https://github.com/Kwamib/gitops-practice/pull/2

c745b1f Bookshop: two copies, and say so on the page (#2)
6d00a98 Lesson 3: the bookshop app, and its Application
1be1855 Lesson 2: ignore the local Argo CD admin password file (#1)

That pull request is the deploy. Someone could have reviewed it, and the change has an author, a time and a reason. Nobody ran anything against the cluster. Then the wait:

OUTPUT
synced to c745b1f and Healthy after 265s

265 seconds: about four and a half minutes. Nothing told Argo CD that the pull request was merged. It checks each Application's repository on a timer, every few minutes by default, and found the new commit on one of those checks. The timer is a setting, timeout.reconciliation in the argocd-cm ConfigMap:

BASH
kubectl get configmap argocd-cm -n argocd -o jsonpath="{.data.timeout\.reconciliation}"; echo " (blank means the default)"
OUTPUT
 (blank means the default)

Blank: the default. Two ways to be faster. argocd app get bookshop --refresh tells Argo CD to check now, which is handy when you're watching. And in real setups, a webhook: GitHub calls Argo CD the moment something is pushed, and the change arrives in seconds. That needs GitHub to be able to reach Argo CD, which a cluster on your laptop isn't, so here the timer it is.

Every deploy is a commit

BASH
argocd app history bookshop
git log --oneline -3
OUTPUT
ID      DATE                           REVISION
0       2026-11-03 19:17:20 -0500 EST  main (6d00a98)
1       2026-11-03 19:22:03 -0500 EST  main (c745b1f)

c745b1f Bookshop: two copies, and say so on the page (#2)
6d00a98 Lesson 3: the bookshop app, and its Application
1be1855 Lesson 2: ignore the local Argo CD admin password file (#1)

Argo CD's two deploys are Git's two commits. "What changed in the last deploy, and who did it?" is git show c745b1f. "What was running on Tuesday?" is whatever commit was on main on Tuesday. Lesson 9 uses this to roll back.

Synced, but not seen

After that sync, the pods:

OUTPUT
NAME                        READY   STATUS    RESTARTS   AGE
bookshop-8467b4689d-fdb9p   1/1     Running   0          5s
bookshop-8467b4689d-mkm6t   1/1     Running   0          4m48s

Two copies, as Git says. And the page:

OUTPUT
<h1>bookshop</h1>
<p>Deployed by Argo CD, from gitops-practice.</p>

The old text. Argo CD said Synced and Healthy, Git had the new line, and the page didn't. Look at the pod names. Both start bookshop-8467b4689d: the same ReplicaSet, from the Kubernetes track's Deployments lesson. Changing replicas only adds a copy; it doesn't replace any. And changing a ConfigMap doesn't restart anything at all. The old pod, mkm6t, kept running, with the page mounted as a file. Kubernetes does update a mounted ConfigMap's files in place, but only after a delay, and the port-forward sends curl to a single pod. It reached the old one before its file had caught up.

Checked later, everything had arrived:

BASH
kubectl get configmap bookshop-page -n bookshop -o jsonpath='{.data.index\.html}'
for p in $(kubectl get pods -n bookshop -o name); do echo "== $p"; kubectl exec -n bookshop $p -- cat /usr/share/nginx/html/index.html; done
OUTPUT
<h1>bookshop</h1>
<p>Deployed by Argo CD, from gitops-practice. Now with two copies.</p>
== pod/bookshop-8467b4689d-fdb9p
<h1>bookshop</h1>
<p>Deployed by Argo CD, from gitops-practice. Now with two copies.</p>
== pod/bookshop-8467b4689d-mkm6t
<h1>bookshop</h1>
<p>Deployed by Argo CD, from gitops-practice. Now with two copies.</p>

The ConfigMap, and both pods' copies of the file, all with the new line, and neither pod restarted. Synced means the cluster's objects match Git. It doesn't promise every running program has picked the change up. For a page served straight from a file, the delay is short. For an app that reads its settings once, at startup, or from environment variables, it never picks the change up until the pod restarts. The standard fix, a checksum of the ConfigMap in the pod template so that a settings change rolls the pods, comes with Helm in Lesson 6.

In real systems

  • Webhooks, not waiting. Teams set up a webhook from GitHub or GitLab to Argo CD, so a merge is deployed in seconds. The timer stays as a safety net, in case a webhook is ever missed.
  • The app's repository and the GitOps repository. Here the YAML and the Application share a repository. Many teams keep each app's code in its own repository, and what runs where in a separate GitOps repository: a deploy is then a small change there, often made by the pipeline. Lesson 7 builds that split.
  • Branch protection makes it safe. If merging to main deploys, then main needs protecting: required reviews, and the lint checks from the Ansible track. Otherwise anyone who can push can deploy.
  • Settings that need a restart. Every team meets "the ConfigMap changed and nothing happened" eventually. Helm's checksum annotation, or a tool that restarts pods when their ConfigMaps change, are the usual answers.

Spot the problem

Work out what's wrong before you read the explanation.

The fix that never left the laptop. An engineer changes the bookshop's page, commits, and runs argocd app get bookshop --refresh. It says Synced, to the same commit as before, and the page hasn't changed. They refresh again, wait ten minutes, then restart the pods by hand. Still the old page. They decide Argo CD must be broken. What's wrong? The commit was never pushed. git commit saved it on their laptop; Argo CD reads GitHub. As far as Argo CD knows, nothing has changed, so Synced to the old commit is exactly right. git status would have said "Your branch is ahead of 'origin/main' by 1 commit". The fix is git push. And restarting the pods by hand changed nothing, because the cluster already matched what Argo CD could see.

Keep this

ToRun or write
Point an Application at your repositoryrepoURL:, targetRevision: main, path: apps/bookshop
Add the Applicationkubectl apply -f argocd/bookshop.yaml
Deploy a changeA branch, a commit, a pull request, merged
Make Argo CD check Git nowargocd app get bookshop --refresh
See how often it checkstimeout.reconciliation in the argocd-cm ConfigMap; blank is the default
Which commit is runningargocd app get bookshop: Synced to main (COMMIT)
Every deploy, with its commitargocd app history bookshop, next to git log --oneline
Reach the appkubectl port-forward svc/bookshop -n bookshop 8081:80
See a file inside each podkubectl exec -n bookshop POD -- cat FILE
Check your commit reached GitHubgit status: no "ahead of origin"

Where you are now

The bookshop runs on your cluster from your own repository, and changes to it arrive by pull request, on Argo CD's timer. Every deploy is a commit, and you've seen that Synced and "users see it" aren't always the same moment. Lesson 4 looks closer at sync itself: what prune and selfHeal really do, what happens when you delete a file from Git, and when you'd want a person to press sync instead.

Check yourself

  1. Where does Argo CD read the bookshop's YAML from?

  2. The pull request was merged. Why did the change take 265 seconds to arrive?

  3. How do you deploy a change to the bookshop?

  4. After the sync, the page still showed the old text. Why?

  5. Both pods were named bookshop-8467b4689d-something. What does that tell you?

  6. Argo CD says Synced, to the old commit, after you committed a fix. What's the likeliest cause?

Key terms

  • targetRevision: the branch, tag or commit an Application follows, like main.
  • path: the folder in the repository that holds an Application's YAML.
  • Reconciliation timer: how often Argo CD checks Git for changes when nothing tells it; timeout.reconciliation.
  • Refresh: asking Argo CD to check Git now, rather than on its next timed check.
  • Webhook: a call from GitHub to Argo CD the moment something is pushed.
  • ConfigMap: settings or files for pods, kept in the cluster; mounted files update in place, after a delay.
  • ReplicaSet: the set of identical pods a Deployment keeps running; a new one appears only when the pod template changes.

Go deeper

  • The Argo CD documentation: Automated sync policy (opens in a new tab), and Git webhook configuration (opens in a new tab).
  • Try: change only the page, merge it, and time how long Argo CD takes again. Is it the same? Then make another change and run argocd app get bookshop --refresh straight after merging.
  • Try: add an environment variable to the Deployment from the ConfigMap, with envFrom, change the ConfigMap, and check the variable inside a running pod with kubectl exec ... -- env. Does it ever update without a restart?