By the end you'll know
- How long a change really takes to reach users, from merge to pods
- What
prunedoes when a file is deleted from Git, and what happens without it - What
selfHealadds on top of automated sync - How manual sync works, and when a person should press the button
- Why changing an Application with the CLI leaves its file out of date
Before you start
- You need: Lessons 1 to 3 done, with the bookshop Application synced from your repository.
- Get the new files:
git pull. You'll havesite/index.html, the new storefront page, and a newapps/bookshop/configmap.yamlmade from it. - Where commands run: your own computer's terminal, in
gitops-practice, logged in withargocd loginas in Lesson 2.
The short version
- A real page. The two-line page became a storefront, through a pull request. Argo CD synced it in 2 seconds after a refresh. The pods served the old page for over a minute after that, because a ConfigMap change doesn't restart them.
- Prune. A Service added in Git appeared; deleted from Git, it was deleted from the cluster. With prune off, it stayed, and Argo CD marked it OutOfSync until a person pruned it.
- Manual sync. With automated sync off, a price change waited in Git, OutOfSync, until someone ran
argocd app sync. - The catch. Each of those settings was changed with the CLI, so the Application in the cluster no longer matched its own file. Lesson 8 fixes that for good.
Three settings, three questions
The bookshop's Application has had the same sync policy since Lesson 3:
syncPolicy:
automated:
prune: true
selfHeal: true
Three settings, and each answers a different question:
automated: when Git changes, does Argo CD apply it by itself, or wait for a person?selfHeal: when the cluster changes by hand, does Argo CD put it back? Lessons 1 and 2 covered this one.prune: when something is removed from Git, does Argo CD remove it from the cluster too?
Think of a gardener with a plan of the garden. automated is whether they plant what's on the plan without asking. selfHeal is whether they replant what someone dug up. And prune is whether they pull out a plant that's no longer on the plan, or leave it, because maybe someone wanted it. This lesson tries each.
A real storefront
First, the page. Lesson 3's page was two lines of HTML. site/index.html is a proper storefront: a header, a shelf of books, six staff picks with prices and stock, and a footer that says which version is running.
wc -c site/index.html
7485 site/index.html
7 KB, styles and all, in one file. It isn't Kubernetes YAML, so it lives outside apps/bookshop/, and the ConfigMap is made from it:
kubectl create configmap bookshop-page --from-file=index.html=site/index.html --dry-run=client -o yaml
--dry-run=client -o yaml doesn't touch the cluster: it prints the ConfigMap kubectl would make, and that output becomes apps/bookshop/configmap.yaml. So site/index.html is the file you edit, and the ConfigMap is regenerated from it. Then the usual way: a branch, a pull request, merged.
merged: 4873922 Bookshop: a real storefront page (#3)
This time, instead of waiting for Argo CD's timer, a refresh straight after the merge:
argocd app get bookshop --refresh
synced to 4873922 after 2s
Two seconds. Compare Lesson 3's 265. So is the new page live? Checking every 10 seconds whether each pod's file has the new page in it (1 or more means yes, 0 means still the old page):
after 10s:
bookshop-8467b4689d-fdb9p: 0
bookshop-8467b4689d-mkm6t: 0
...
after 60s:
bookshop-8467b4689d-fdb9p: 0
bookshop-8467b4689d-mkm6t: 0
after 70s:
bookshop-8467b4689d-fdb9p: 3
bookshop-8467b4689d-mkm6t: 0
after 80s:
bookshop-8467b4689d-fdb9p: 3
bookshop-8467b4689d-mkm6t: 3
Synced in 2 seconds; seen by users in 70 to 80. For over a minute, Argo CD said Synced, the cluster's ConfigMap had the new page, and anyone visiting the shop saw the old one. Then the two pods switched, ten seconds apart, without restarting:
NAME READY STATUS RESTARTS AGE
bookshop-8467b4689d-fdb9p 1/1 Running 0 3h43m
bookshop-8467b4689d-mkm6t 1/1 Running 0 3h48m
Hours old, the same pods as Lesson 3. That's the delay Lesson 3 ran into, measured: Kubernetes refreshes a mounted ConfigMap's files on its own schedule, roughly every minute. Fine for a page; a problem for anything that needs the change at once, which is what Lesson 6's checksum fix is for.
Prune: deleted from Git, deleted from the cluster
Add something, then remove it. A second Service, bookshop-preview, in its own file:
# A second way in, for previewing the shop. Removed again in this lesson.
apiVersion: v1
kind: Service
metadata:
name: bookshop-preview
spec:
selector:
app: bookshop
ports:
- port: 8080
targetPort: 80
Merged, refreshed:
merged: 1c30a8e Bookshop: a preview Service (#4)
synced to 1c30a8e after 1s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bookshop ClusterIP 10.96.141.31 <none> 80/TCP 3h48m
bookshop-preview ClusterIP 10.96.16.39 <none> 8080/TCP 0s
There it is. Now delete the file, with git rm, and merge that:
merged: 1eacdb7 Bookshop: remove the preview Service (#5)
synced to 1eacdb7 after 0s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bookshop ClusterIP 10.96.141.31 <none> 80/TCP 3h48m
Gone from the cluster too. That's prune: true. Argo CD knows the Service was its own, from the tracking annotation Lesson 2 showed, so when Git stops mentioning it, Argo CD deletes it. The same thing the Ansible track had to do by hand with state: absent, done by deleting a file.
Prune switched off
Same again, with prune off:
argocd app set bookshop --auto-prune=false
{'automated': {'prune': False, 'selfHeal': True}, 'syncOptions': ['CreateNamespace=true']}
The preview Service goes back in (#6), then is removed again (#7). The cluster:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bookshop ClusterIP 10.96.141.31 <none> 80/TCP 3h48m
bookshop-preview ClusterIP 10.96.73.110 <none> 8080/TCP 20s
Still there, though Git no longer has it. And Argo CD says so:
argocd app get bookshop
Sync Status: OutOfSync from main (90dcbf0)
Health Status: Healthy
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service bookshop bookshop-preview OutOfSync Healthy service/bookshop-preview unchanged
ConfigMap bookshop bookshop-page Synced
Service bookshop bookshop Synced Healthy
apps Deployment bookshop bookshop Synced Healthy
The whole app is OutOfSync, and the table shows why: one Service, OutOfSync, that Git doesn't have. Automated sync did run, and synced everything else, but it won't delete anything without permission. A person gives it:
argocd app sync bookshop --prune
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service bookshop bookshop-preview Succeeded Pruned pruned
ConfigMap bookshop bookshop-page Synced configmap/bookshop-page configured
Service bookshop bookshop Synced Healthy service/bookshop unchanged
apps Deployment bookshop bookshop Synced Healthy deployment.apps/bookshop unchanged
Pruned. So which is right? Prune on means Git is the whole truth, and deleting a file is a real delete. Prune off protects against deleting by accident, such as a folder renamed in a pull request, at the cost of leftovers someone has to clean up. Most teams run with prune on, and protect the few things that must never be deleted, like a database's storage, with a per-resource setting instead.
Manual sync: a person decides
The last setting: no automated sync at all.
argocd app set bookshop --sync-policy none
argocd app get bookshop | grep "Sync Policy"
Sync Policy: Manual
Then a change in Git: The Linux Command Line drops from $29 to $24, in site/index.html and the regenerated ConfigMap.
merged: 3e6701d Bookshop: The Linux Command Line, now $24 (#8)
Sync Status: OutOfSync from main (3e6701d)
Health Status: Healthy
Merged, noticed, and not applied. Argo CD knows Git has moved on, and waits. What would change?
argocd app diff bookshop
===== /ConfigMap bookshop/bookshop-page ======
< <div class="row"><span class="price">$29</span><span class="stock">4 in stock</span></div>
> <div class="row"><span class="price">$24</span><span class="stock">4 in stock</span></div>
diff exit code: 1
(Just the price lines; the full diff is the whole page.) The cluster says $29, Git says $24. A person reads that, and decides:
argocd app sync bookshop
argocd app get bookshop | grep -E "Sync Status|Sync Policy"
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
ConfigMap bookshop bookshop-page Synced configmap/bookshop-page configured
Sync Policy: Manual
Sync Status: Synced to main (3e6701d)
Manual sync is a reasonable choice for production at some teams: Git is still the only source, but a deploy also needs someone to press the button, after reading the diff. The cost is that merged changes can sit waiting, and someone has to remember.
The catch: the Application drifted from its file
Every setting in this lesson was changed with argocd app set. That changed the Application in the cluster. Its file in Git still says:
grep -A3 "syncPolicy" argocd/bookshop.yaml
syncPolicy:
automated:
prune: true
selfHeal: true
So the bookshop's contents are managed by GitOps, but the Application describing them isn't: it was applied by hand in Lesson 3, and nothing watches it. Right now the cluster says Manual, and the file says automated. Here, putting it back is one command:
kubectl apply -f argocd/bookshop.yaml
argocd app get bookshop | grep "Sync Policy"
application.argoproj.io/bookshop configured
Sync Policy: Automated (Prune)
But it's the same drift Argo CD exists to prevent, one level up. The fix is to let Argo CD manage the Applications themselves, from Git: an Application whose folder holds the other Applications. That's app-of-apps, Lesson 8. Until then, change Applications by editing their files and applying them, not with argocd app set; this lesson used the CLI to show each setting quickly.
Every change, in order
argocd app history bookshop
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)
2 2026-11-03 23:04:17 -0500 EST main (4873922)
3 2026-11-03 23:05:43 -0500 EST main (1c30a8e)
4 2026-11-03 23:05:50 -0500 EST main (1eacdb7)
5 2026-11-03 23:05:59 -0500 EST main (abc1322)
6 2026-11-03 23:06:20 -0500 EST main (90dcbf0)
7 2026-11-03 23:06:39 -0500 EST main (3e6701d)
Eight deploys, each one a merged pull request, each one a commit you can read with git show.
In real systems
- Prune on, with exceptions. A per-resource annotation,
argocd.argoproj.io/sync-options: Prune=false, stops Argo CD ever deleting that one resource, such as a database's storage claim, while everything else is pruned normally. - Settings that need a restart get a checksum. For an app that reads its settings at startup, the minute-long ConfigMap delay becomes "never, until a restart". Helm charts put a checksum of the ConfigMap in the pod template, so changing settings rolls the pods. Lesson 6 does it.
- Manual sync for production, automated for the rest. A common split: development and staging sync automatically on every merge, production waits for a person, or for a sync window that only allows deploys at certain times.
- Applications live in Git too. Teams don't
argocd app setin production. The Applications are files in a repository, managed by a parent Application, so their settings get reviews and history like everything else.
Spot the problem
Work out what's wrong before you read the explanation.
The tug of war. A team adds a HorizontalPodAutoscaler to the bookshop, so it scales between 2 and 10 copies with traffic. Their Deployment in Git still says replicas: 2, and the Application has selfHeal: true. On a busy afternoon, the autoscaler raises the Deployment to 6 copies. Seconds later there are 2 again. Then 6, then 2. The shop is slow, and the pods keep starting and stopping. What's happening? Argo CD and the autoscaler are fighting over the same field. To Argo CD, 6 copies is drift from Git's 2, and self-heal puts it back, exactly as it did with kubectl scale in Lesson 1. The autoscaler then raises it again. The fix: tell Argo CD that replicas isn't its business on this Deployment. Either remove replicas from the Deployment in Git, so the autoscaler owns it, or add an ignoreDifferences entry for /spec/replicas to the Application. One owner per field.
Keep this
| To | Write or run |
|---|---|
| Apply Git changes without a person | syncPolicy: automated: {} |
| Delete what Git no longer has | prune: true |
| Undo hand changes to the cluster | selfHeal: true |
| Check Git now, not on the timer | argocd app get bookshop --refresh |
| Sync by hand | argocd app sync bookshop |
| Sync by hand, and prune | argocd app sync bookshop --prune |
| Never prune one resource | argocd.argoproj.io/sync-options: Prune=false, as an annotation |
| Let something else own a field | ignoreDifferences: in the Application |
| Make a ConfigMap file from a page | kubectl create configmap NAME --from-file=index.html=FILE --dry-run=client -o yaml |
| Change an Application properly | Edit its file, then kubectl apply -f argocd/bookshop.yaml |
| Quick changes, for testing only | argocd app set bookshop --auto-prune=false, --sync-policy none |
Where you are now
The bookshop looks like a shop, and you've seen each sync setting do its job: automated or manual, self-heal, and prune, with prune's leftovers and manual sync's waiting. You've also seen that a change can be synced well before users see it. The bookshop is still plain YAML, with the page pasted into a ConfigMap by hand. Lesson 5 turns it into a Helm chart: templates and values, so the shop's name, version and copies are settings, not edits.
Check yourself
Argo CD synced the new page in 2 seconds. When did users see it?
With prune on, a Service's file is deleted from Git. What happens?
With prune off, the same thing happens. What does Argo CD do?
With manual sync, a price change is merged. What happens?
After argocd app set, why didn't the Application match argocd/bookshop.yaml?
An autoscaler and Argo CD keep changing replicas back and forth. What's the fix?
Key terms
- Automated sync: Argo CD applying changes from Git without waiting for a person.
- Manual sync: Argo CD noticing changes, and waiting for someone to run a sync.
- Prune: deleting from the cluster what Git no longer has.
- Self-heal: undoing changes made to the cluster outside Git.
- Refresh: checking Git now, instead of on the next timed check.
- Leftover: a resource still in the cluster after Git stopped describing it, because prune was off.
- ignoreDifferences: an Application setting that tells Argo CD to ignore certain fields, owned by something else.
- App-of-apps: an Application whose folder holds other Applications, so they're managed from Git too.
Go deeper
- The Argo CD documentation: Automated sync policy (opens in a new tab), Sync options (opens in a new tab), and Diffing customization (opens in a new tab).
- Try: add
argocd.argoproj.io/sync-options: Prune=falseto the preview Service's annotations, put it back in Git, then delete its file again, with prune on. What doesargocd app get bookshopshow? - Try: change the storefront's headline in
site/index.html, regenerate the ConfigMap, merge it, and time how long untilcurlshows it. Is it close to a minute again?