This is part one of three. It covers everything you need to do real work with Argo CD, not a teaser. By the end you can install Argo CD into a cluster, connect it to a Git repository, deploy an application from that repository, watch it drift and heal, roll a bad change back, and explain to a colleague why the cluster looks the way it does. Mid-level and Senior take the same topics further; nothing here is thrown away.
The guide is written against Argo CD v3.5.3, the current stable release at the time of writing (3.5 went generally available on 4 August 2026, and the next minor, 3.6, is scheduled for 3 November 2026). Most tutorials on the web still describe the 2.x series, and a few of their commands no longer work. Where that matters, the text says so and shows the current form.
Each section ends with a Try it task. Do them as you go. They take a few minutes each, and the ideas only stick once you have watched your own application go out of sync and come back.
You will need a Kubernetes cluster you are allowed to break (a local one such as kind, minikube or Docker Desktop's built-in Kubernetes is perfect), kubectl configured to talk to it, and a GitHub account for the later sections. If Kubernetes itself is new to you, read the Kubernetes guide first; Argo CD is a program that manages Kubernetes objects, so it assumes you know what a Deployment and a Service are. If you write your manifests with Helm or Kustomize, the Helm guide and Kustomize guide cover the tools Argo CD will run on your behalf.
What Argo CD is, and the problem it solves
Argo CD is a continuous delivery controller for Kubernetes that uses Git as the source of truth. You describe what should run in the cluster as files in a Git repository. Argo CD watches that repository, compares what it says with what is actually running, tells you where the two differ, and, if you allow it, changes the cluster until they match.
To see why this is useful, look at what teams did before. The classic approach to deploying to Kubernetes is a script in your continuous integration system. The pipeline builds an image, then runs kubectl apply or helm upgrade against the cluster. That works, and it is how many teams still operate, but it has three structural problems.
The first is credentials. For the pipeline to change the cluster, the CI system must hold a credential that can change the cluster. CI systems are large, shared, internet-facing programs that run other people's code in pull requests. Handing them the keys to production is a real risk, and it is one that many security reviews flag.
The second is drift. After the pipeline finishes, nobody is watching. Somebody runs kubectl edit deployment at two in the morning to fix an incident and forgets to put the change back in the repository. A controller modifies an object. A teammate scales something by hand. The repository now says one thing and the cluster does another, and the next pipeline run silently overwrites the hotfix, or does not touch it at all. You only discover the difference when something breaks.
The third is visibility. "What is running in production right now, and is it what we merged?" should be a thirty-second question. With push-based scripts, the honest answer is often "read the pipeline logs and hope nobody changed anything since".
Argo CD attacks all three by reversing the direction. It runs inside the cluster and pulls from Git. The pipeline's only job becomes building an image and committing a new version number to the repository. It never talks to the cluster at all, so it needs no cluster credentials. Argo CD keeps looking at both sides, so drift is detected rather than discovered. And the whole state of the system is one screen: every application, whether it matches Git, and whether it is healthy.
This way of working has a name, GitOps. The idea is simple enough to state in one sentence: the desired state of your system lives in Git, and software continuously makes reality match it. Git then gives you, for free, everything it already gives you for code: history, review, blame, revert, and a permission model.
Argo CD is one implementation of the idea. Its closest neighbour is Flux, which takes the same approach with a different design: Flux is a set of small controllers with no built-in web interface, while Argo CD is a single product with a strong UI and a command-line client. Both are open source projects of the Cloud Native Computing Foundation. This guide only covers Argo CD.
A note on what Argo CD is not. It does not build your container images; that remains the job of your CI system and of tools like Docker. It does not create your cluster; that is the job of something like Terraform. It does not replace Helm or Kustomize; it runs them. It is the last step in the chain: given a repository that already describes the desired state, get it into the cluster and keep it there.
- Think of one deployment you have made with
kubectl applyor a pipeline. - Write down who or what held the credential that changed the cluster.
- Write down how you would find out if someone edited the live object by hand last week.
The GitOps loop: desired state, live state, and the difference
Two words carry most of Argo CD's vocabulary, and the official documentation uses them precisely. The target state (also called the desired state) is what the files in Git describe. The live state is what is actually running in the cluster right now: which Deployments exist, how many replicas they have, which image they use.
Argo CD's whole job is a loop with four steps, repeated forever:
- Fetch the files from the Git repository at the revision you asked for.
- Render them into plain Kubernetes manifests. If the files are already plain YAML, this is nothing. If they are a Helm chart or a Kustomize overlay, Argo CD runs
helm templateorkustomize buildto produce YAML. - Compare those manifests with the live objects in the cluster. This comparison is called a diff.
- Report the result, and, only if configured to, apply the manifests to remove the difference.
The first three steps are always on. Step four is where you choose how much to trust the tool. You can let Argo CD only report and press a button yourself to apply (manual sync, the default), or let it apply automatically every time Git changes (automated sync). Starting with manual is wise, because it lets you see what Argo CD would do before it does it.
The word for step four is sync. "To sync an application" means to make the live state match the target state, in practice by applying the rendered manifests to the cluster. You will type and click that word hundreds of times.
There is an important consequence of the pull model that beginners often miss. Argo CD does not run when you push; it runs on its own schedule. By default it checks Git roughly every three minutes (the exact number is 120 seconds plus up to 60 seconds of random jitter, so the checks from many applications do not all land at once). You can make that near-instant with a webhook, which the later section on refreshing covers. Until then, if you push a commit and nothing happens for two minutes, nothing is broken.
The nouns: Application, Project, status, health
Argo CD has four core nouns. Learn them now, because every screen, command and error message uses them.
An Application is the central object. It is a record that says: take these files from this repository, and deploy the result to this namespace in this cluster. The file location is called the source, and the place it is deployed is called the destination. The Application does not contain your manifests; it only points at them. Technically an Application is a Kubernetes custom resource (kind Application, API group argoproj.io), which means you can create one the same way you create any other Kubernetes object, with YAML and kubectl. That detail turns out to matter a lot, because it means you can store the Applications themselves in Git too.
The source of an application has a type, called the tool. Argo CD detects it from the files: a directory of plain YAML files, a Kustomize directory (it has a kustomization.yaml), a Helm chart (it has a Chart.yaml), or Jsonnet. You rarely pick this yourself.
An AppProject, usually just called a project, is a group of Applications with shared rules. A project answers questions such as "which repositories may these applications deploy from?" and "which clusters and namespaces may they deploy to?" Every Application belongs to exactly one project. A brand-new installation has one project, named default, which allows everything. In a company, projects are how you stop the payments team from deploying into the security team's namespace. As a beginner you will use default and learn to recognise the errors that come from a stricter one.
The sync status says whether the live state matches Git. It has three values: Synced (they match), OutOfSync (they differ) and Unknown (Argo CD could not tell, usually because it could not read the repository or render the manifests).
The health status says whether the application is actually working. It has six values: Healthy, Progressing (still starting up), Degraded (something has failed), Suspended (paused on purpose), Missing (the resources do not exist) and Unknown. Argo CD decides this by looking at the resources: a Deployment is healthy when its replicas are available, a Pod is degraded when it is crash-looping, and so on.
Keep those two statuses separate in your head. They answer different questions, and mixing them up is the most common beginner misreading of the dashboard.
Synced and Degraded at the same time. That means "the cluster matches Git exactly, and what Git describes is broken": for example a Deployment pointing at an image tag that does not exist. Argo CD did its job perfectly; the repository is wrong. Conversely an application can be Healthy and OutOfSync: everything runs, but someone changed something by hand.
| Status | Question it answers | Values |
|---|---|---|
| Sync status | Does the cluster match Git? | Synced, OutOfSync, Unknown |
| Health status | Is the app actually working? | Healthy, Progressing, Degraded, Suspended, Missing, Unknown |
| Operation phase | Did the last sync succeed? | Running, Succeeded, Failed, Error, Terminating |
The third row is the result of the last sync operation rather than a property of the application as a whole. You see it in the sync history and when a sync fails.
Two more words appear constantly. A refresh means "compare the latest Git contents with the live state again". A hard refresh does the same but also throws away Argo CD's cached copy of the rendered manifests, which matters when a cached error is stuck. You will use it in the troubleshooting section.
- Without looking back, write the four nouns and one sentence for each.
- Write one example of an application that is
Syncedbut notHealthy. - Write one example of one that is
HealthybutOutOfSync.
What is inside Argo CD: the components you will meet
You can use Argo CD for months without thinking about its internals, but three of its pieces appear in every error message and log line, so it is worth knowing what they do. When you install Argo CD, you get a set of pods in a namespace (conventionally called argocd).
The API server (argocd-server) is what you talk to. It serves the web interface and the API that the argocd command-line tool uses. It handles logging in and decides what each user may do. It is stateless: nothing precious lives in it.
The repo server (argocd-repo-server) is the component that touches your Git repository. It clones the repository, keeps a cached copy, and produces the rendered manifests by running Helm, Kustomize or plain file reading. Notably it has no permissions in your cluster. It only reads Git and writes YAML. That separation is deliberate: the component that processes untrusted repository content cannot modify your cluster.
The application controller (argocd-application-controller) is the brain. It watches your cluster, asks the repo server for the target state, compares it with the live state, updates each application's sync and health status, and performs syncs. It runs as a StatefulSet.
Everything else is supporting cast. The ApplicationSet controller stamps out many Applications from one template (a mid-level topic). The notifications controller sends messages to Slack, email and so on. Dex is a bundled single sign-on broker. Redis is a cache, and it is disposable: Argo CD keeps no important state there. All the real state (your Applications, Projects, repository credentials, configuration) is stored as ordinary Kubernetes objects and ConfigMaps. That is why you can back up Argo CD by exporting its objects, and why deleting a pod never loses your setup.
When something goes wrong, this map tells you where to look. A repository that cannot be read or a chart that will not render is a repo-server problem: look at its logs. An application that will not sync or shows the wrong health is a controller matter. A login or permission failure is the API server.
kubectl logs -n argocd deploy/argocd-repo-server
kubectl logs -n argocd statefulset/argocd-application-controller
kubectl logs -n argocd deploy/argocd-server
- Draw the three main components on paper and draw an arrow for "reads Git", one for "changes the cluster" and one for "serves the UI".
- Label which component each arrow belongs to.
Installing Argo CD into a cluster
Argo CD installs as a set of Kubernetes manifests into its own namespace. Start by checking that kubectl points where you think it does. Installing into the wrong cluster is the classic first mistake, and it is cheap to avoid.
kubectl config current-context
kubectl get nodes
If you do not have a cluster, create a local one. With kind (Kubernetes in Docker) it is one command, and you can throw it away afterwards:
kind create cluster --name argo-demo
Now create the namespace and install. Note the flags carefully, because this is the most commonly outdated command in tutorials:
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.3/manifests/install.yaml
The --server-side --force-conflicts part is mandatory since Argo CD 3.3. Argo CD's ApplicationSet definition became larger than the amount of data that client-side kubectl apply is allowed to store in an annotation (262,144 bytes). A plain kubectl apply -f install.yaml, which is what older tutorials show, now fails with this message:
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
Server-side apply moves the bookkeeping from an annotation to the API server, which has no such limit. If you meet that error, you have copied an old command; add the two flags and run it again.
Also notice the version in the URL. The official documentation uses stable in its examples, which always points to the newest release. For anything beyond a throwaway experiment, pin a version tag as shown, so that re-running the command next month does not quietly upgrade you.
The install.yaml manifest is the standard, non-high-availability installation, good for learning and evaluation. The project also publishes a high-availability variant (ha/install.yaml, which needs at least three nodes) for production, a namespace-scoped variant for restricted setups, and a headless core-install.yaml. A community-maintained Helm chart (argo/argo-cd from https://argoproj.github.io/argo-helm) is another popular route; note that it is maintained by the community, not by the Argo project itself. All of those are mid-level and senior topics.
Wait for the pods to come up:
kubectl get pods -n argocd
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 90s
argocd-applicationset-controller-6c8f7c4b9d-x2k7p 1/1 Running 0 90s
argocd-dex-server-7d9f5b8c6f-q4m8n 1/1 Running 0 90s
argocd-notifications-controller-5f7b9d6c4-h7t2v 1/1 Running 0 90s
argocd-redis-66b7c5d8f9-w9r4z 1/1 Running 0 90s
argocd-repo-server-5d8c7f9b6-m3n5p 1/1 Running 0 90s
argocd-server-6b9d8c7f5-k8j2l 1/1 Running 0 90s
Your pod name suffixes will differ. What matters is that every pod is Running and READY shows all containers ready. The first minute often shows ContainerCreating or Init, which is normal while images download. If something stays in ImagePullBackOff, your cluster cannot reach the registry; if it stays Pending, the cluster has no room.
You can also confirm that the custom resource definitions, the new object types Argo CD adds to Kubernetes, exist:
kubectl get crd | grep argoproj.io
applications.argoproj.io 2026-10-01T09:14:02Z
applicationsets.argoproj.io 2026-10-01T09:14:02Z
appprojects.argoproj.io 2026-10-01T09:14:02Z
kubectl get pods -n argocd -w in a second terminal while the install runs. The -w flag keeps the list updating, so you see each pod move from Pending to Running without retyping the command.
- Install Argo CD into a local cluster with the pinned, server-side command above.
- Run
kubectl get pods -n argocduntil every pod isRunning. - Run
kubectl get crd | grep argoproj.ioand count the three definitions.
Installing the CLI and checking the setup
The web interface is enough to get started, but the argocd command-line tool is how you script, automate and debug. It is a separate download from the server, and its version should match the server's closely.
On macOS, and on Linux or WSL if you use Homebrew, the simplest route is:
brew install argocd
On Linux without Homebrew, download the pinned binary directly and install it:
VERSION=v3.5.3
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64
On Apple Silicon without Homebrew the binary is named argocd-darwin-arm64, and on Intel Macs argocd-darwin-amd64. On Windows, the official documentation describes downloading argocd-windows-amd64.exe from the releases page and adding its folder to your Path; package-manager packages for Windows exist, but they are maintained by the community rather than the Argo project, so check their version before trusting them. If you use WSL, simply follow the Linux steps.
Confirm the client works:
argocd version --client
The output names the client version, such as argocd: v3.5.3+.... The server half of the answer only appears once you are logged in, which is the next step.
Now reach the server. The installation creates a Service called argocd-server, but by default it is only reachable from inside the cluster. The quickest way to get at it from your laptop is a port-forward, which tunnels a local port to the service:
kubectl port-forward svc/argocd-server -n argocd 8080:443
Leave that command running in its own terminal. The interface is now at https://localhost:8080. Your browser will warn about the certificate. That is expected: a default installation generates a self-signed certificate that no browser trusts. For a local experiment, click through the warning. For a real deployment you would configure a proper certificate or terminate TLS at an ingress; that is an operations topic, and the warning is not a bug.
Other ways to expose the server exist (changing the Service to a LoadBalancer, or configuring an Ingress), and they matter for shared installations. For learning, the port-forward is ideal because nothing is exposed beyond your own machine.
--port-forward-namespace argocd to CLI commands, or set ARGOCD_OPTS='--port-forward-namespace argocd' once in your shell. The CLI then opens the tunnel itself for each command, and you use localhost style addresses without running kubectl port-forward.
- Install the CLI and run
argocd version --client. - Start the port-forward and open
https://localhost:8080in your browser. - Accept the certificate warning and confirm a login page appears.
Logging in for the first time
A fresh installation has one user, admin, with a randomly generated password. The password is stored in a Kubernetes Secret named argocd-initial-admin-secret. The CLI can read it for you:
argocd admin initial-password -n argocd
Copy the password it prints. (Older tutorials tell you the password is the name of the argocd-server pod. That was true before version 1.9 and has not been true for years.)
Log in with the CLI, with the port-forward still running. Because of the self-signed certificate, the CLI will refuse to connect at first, printing a message like x509: certificate signed by unknown authority. For local learning only, tell it to skip verification:
argocd login localhost:8080 --username admin --insecure
It prompts for the password. On success you see 'admin:login' logged in successfully and a line naming the context. The --insecure flag disables certificate checking, so never use it against a server you do not control; in production you fix the certificate instead.
Now argocd version shows both halves:
argocd version
argocd: v3.5.3+abc1234
BuildDate: 2026-09-14T10:02:11Z
GitCommit: abc1234
...
argocd-server: v3.5.3+abc1234
...
If the client and server minor versions differ, the tool usually still works, but mismatches are a source of odd behaviour, so keep them aligned.
The first thing to do after logging in is change the generated password, because the initial one is only meant to get you in:
argocd account update-password
It asks for the current password and the new one twice. After that, the initial secret is no longer needed, and the official guidance is to delete it:
kubectl -n argocd delete secret argocd-initial-admin-secret
Log into the web interface at the same address with username admin and your password. You will see an empty applications page with a + New App button. That emptiness is about to change.
- Read the initial password with
argocd admin initial-password -n argocd. - Log in with the CLI, run
argocd version, and confirm you see a server section. - Change the password, then delete the initial secret.
- Log into the web interface.
Your first application: the guestbook
The Argo project maintains a public repository of example applications, argocd-example-apps. Its guestbook directory contains a tiny web application as two plain Kubernetes manifests, which makes it the ideal first deployment. You will deploy it into the default namespace of the same cluster Argo CD runs in.
One detail first. Argo CD calls the cluster it runs in the in-cluster destination, and its API address from inside the cluster is always https://kubernetes.default.svc. You can see it is registered already:
argocd cluster list
SERVER NAME VERSION STATUS MESSAGE PROJECT
https://kubernetes.default.svc in-cluster 1.35 Successful
The version shown will be whatever your cluster runs. Now create the Application. The CLI command spells out the four things an Application needs: where the files are, which folder, and where to put the result.
argocd app create guestbook \
--repo https://github.com/argoproj/argocd-example-apps.git \
--path guestbook \
--dest-server https://kubernetes.default.svc \
--dest-namespace default
You should see application 'guestbook' created. Notice what has not happened: nothing is deployed yet. You have only told Argo CD about the application. Ask it what it sees:
argocd app get guestbook
Name: argocd/guestbook
Project: default
Server: https://kubernetes.default.svc
Namespace: default
URL: https://localhost:8080/applications/guestbook
Source:
- Repo: https://github.com/argoproj/argocd-example-apps.git
Target:
Path: guestbook
SyncWindow: Sync Allowed
Sync Policy: Manual
Sync Status: OutOfSync from (53e28ff)
Health Status: Missing
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service default guestbook-ui OutOfSync Missing
apps Deployment default guestbook-ui OutOfSync Missing
Read that output line by line, because this is the screen you will read most. Sync Policy: Manual means Argo CD will not change anything until told to. Sync Status: OutOfSync means Git says there should be a Service and a Deployment and the cluster has neither. Health Status: Missing means the resources do not exist. The table at the bottom lists each resource Argo CD found in the repository, and each is OutOfSync and Missing for the same reason. This is exactly what "desired but not live" looks like. The commit hash in brackets is the revision Argo CD compared against.
Before syncing, ask for the diff, to see what would change:
argocd app diff guestbook
Because nothing exists yet, the output shows both objects as additions. The command's exit code carries information too: 0 means no difference, 1 means a difference was found, and 2 means an error. Scripts rely on that.
Now sync:
argocd app sync guestbook
The output streams each resource as Argo CD applies it, ending with a table whose rows say Synced and Healthy. Check the result in two places:
argocd app get guestbook
kubectl get deploy,svc,pods -n default
The status is now Synced and Healthy, and Kubernetes shows a guestbook-ui Deployment, Service and Pod that you never applied yourself. Open the web interface and click the guestbook tile: you get the resource tree, a graph from the Application down to the Deployment, ReplicaSet and Pod, with green hearts for healthy nodes. That picture is the reason many teams adopt Argo CD.
To see the application itself in a browser, forward its service:
kubectl port-forward svc/guestbook-ui -n default 8081:80
Then visit http://localhost:8081.
- Create the guestbook application with
argocd app create. - Run
argocd app get guestbookand confirmOutOfSyncandMissing. - Run
argocd app diff guestbook, thenargocd app sync guestbook. - Open the application in the web UI and look at the resource tree.
Reading the UI: sync status, health and the resource tree
The command line and the web interface show the same facts, and you should be comfortable reading both. The applications page shows one tile per application with two badges: the sync status and the health status. A green check means Synced, a yellow circle with an arrow means OutOfSync, a green heart means Healthy, a blue spinning circle means Progressing, and a broken red heart means Degraded.
Click into an application and you get the resource tree. On the left is the Application; to its right are the Kubernetes objects it manages, and to their right the objects those created. A Deployment owns a ReplicaSet, which owns Pods. Every node has its own small sync and health icons, so when an application is unhealthy you follow the red icons down the tree to the single pod or object that is the cause. That is usually faster than reading a dozen kubectl describe outputs.
Three buttons at the top of the application page do the everyday work. Refresh re-compares Git and the cluster (a normal refresh; the dropdown offers a hard refresh). Sync opens a panel where you choose what to sync and with which options, then applies. History and Rollback lists previous syncs, which you will use shortly.
Click any resource node and a panel slides out with tabs: Summary, Events, Logs (for pods) and Diff. The Diff tab deserves special attention. It shows the live object beside the desired one, highlighting what differs. When an application says OutOfSync and you do not know why, the diff tab answers the question, and it is also how you learn which fields the cluster adds to your objects (defaults, annotations) that can cause confusing permanent differences.
There is also a Details panel for the application itself, showing its source, destination and sync policy, and a Parameters tab for Helm or Kustomize settings. You can edit the Application from there, and Argo CD will store the change, but keep in mind the declarative approach in the next section: edits made in the UI live only in the cluster, not in Git.
One last reading skill: the sync status colours can disagree with what you expect for a moment after a change. Git is polled, the live cluster is watched, and there is a small cache in between. If a status looks stale, press Refresh before concluding anything.
- Open the guestbook application and click the Deployment node.
- Look at the Summary, Events and Diff tabs.
- Click the Pod node and open its Logs tab.
kubectl logs.
An Application as a YAML file
You created the guestbook with a command, which is fine for exploring. But the command only stored the Application inside the cluster. If you deleted the cluster, the record of what you had deployed would be gone. The GitOps answer is to write the Application itself as a file and keep it in Git too.
Remember that an Application is a Kubernetes custom resource. Here is the guestbook as a manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/argoproj/argocd-example-apps.git
targetRevision: HEAD
path: guestbook
destination:
server: https://kubernetes.default.svc
namespace: guestbook
syncPolicy:
syncOptions:
- CreateNamespace=true
Each part matters, so go through it slowly.
The metadata.namespace is argocd, the namespace where Argo CD is installed. This catches people out. The Application object itself lives in Argo CD's namespace, while the things it deploys go to spec.destination.namespace. They are different namespaces with different purposes. (Later versions allow Applications in other namespaces, but only when an administrator enables it.)
The finalizer, resources-finalizer.argocd.argoproj.io, controls what happens on deletion. With it, deleting the Application also deletes every resource it deployed (a cascading delete). Without it, deleting the Application object leaves the deployed resources running in the cluster, orphaned. Neither is wrong, but you should choose deliberately; the deletion section returns to this.
spec.project names the project. default is the permissive one that exists from the start.
spec.source says where the files are. repoURL is the Git repository. targetRevision is which version to track: a branch name, a tag, a commit hash, or HEAD (the default branch's latest commit). path is the folder inside the repository. If you deploy a Helm chart from a chart repository instead of a folder in Git, you write chart: <name> in place of path, and targetRevision becomes the chart version.
spec.destination says where the result goes. server is the cluster's API address and namespace is the target namespace. You can use name: in-cluster instead of server, but never both.
spec.syncPolicy.syncOptions holds per-application tweaks. CreateNamespace=true tells Argo CD to create the destination namespace if it does not exist. Without it, syncing into a missing namespace fails with a "namespace not found" error. That is a very common first failure, and this option is the fix.
Apply the file with kubectl, exactly as you would any other Kubernetes object:
kubectl apply -f guestbook-app.yaml
If the CLI-created guestbook still exists, this updates it (the names match). Kubernetes stores it, Argo CD notices it within seconds, and the Application appears in the UI. Now the Application definition is a file you can review, version and recreate.
The CLI can show you the YAML for an existing Application too, which is a handy way to learn the format:
argocd app get guestbook -o yaml
kubectl get application guestbook -n argocd -o yaml
The second command proves the point made earlier: an Application is just a Kubernetes object, and kubectl can read it.
argocd app create, read back its YAML with argocd app get NAME -o yaml, and trim it into a clean file. The output includes status fields and other generated details that do not belong in a file you commit; keep only metadata.name, the finalizer and the spec.
- Delete the CLI-created application with
argocd app delete guestbook. - Save the YAML above as
guestbook-app.yamland apply it withkubectl apply. - Sync it with
argocd app sync guestbookand check the newguestbooknamespace exists.
CreateNamespace=true, and the application back to Synced and Healthy.
Deploying your own repository
The guestbook is someone else's repository. The real value appears when Argo CD watches yours. The workflow from here is the one you will use at work: put manifests in a Git repository, point an Application at them, then change the deployed state only by committing.
Create a new public GitHub repository (public keeps the credentials question out of the way for now; the private case is coming) with a folder app/ containing two files.
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
labels:
app: hello
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: nginx:1.27
ports:
- containerPort: 80
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
selector:
app: hello
ports:
- port: 80
targetPort: 80
Commit and push them. Then create the Application, replacing the URL with your repository's:
argocd app create hello \
--repo https://github.com/YOUR-USER/YOUR-REPO.git \
--path app \
--dest-server https://kubernetes.default.svc \
--dest-namespace hello \
--sync-option CreateNamespace=true
argocd app sync hello
Argo CD clones your repository (through the repo server), sees plain YAML files in app/, and treats the directory as its source. You now have two pods running from files in your repository.
Now make a change the GitOps way. Edit replicas: 2 to replicas: 3 in deployment.yaml, commit and push. Do not touch the cluster. Watch what Argo CD does:
argocd app get hello --refresh
The --refresh flag asks Argo CD to re-check Git right now instead of waiting for the polling interval. The status becomes OutOfSync, because Git says three replicas and the cluster has two. Look at the difference, then apply it:
argocd app diff hello
argocd app sync hello
The diff shows replicas: 2 becoming replicas: 3. After the sync, kubectl get pods -n hello lists three pods. Nothing touched the cluster except Argo CD, and the record of why there are three is a commit with an author and a message. That is the GitOps loop in its smallest form.
Argo CD is not limited to plain YAML folders. If the path contains a kustomization.yaml, it runs Kustomize; if it contains a Chart.yaml, it runs Helm; if you point at a Helm chart repository with --helm-chart, it pulls that chart. The bundled tools in v3.5 are Helm 4 and Kustomize 5. You still write the application definition the same way; Argo CD figures out the tool.
argocd app create nginx-chart \
--repo https://charts.bitnami.com/bitnami \
--helm-chart nginx \
--revision 18.2.0 \
--dest-name in-cluster \
--dest-namespace web \
--sync-option CreateNamespace=true
Treat the repository address and chart version above as placeholders for a chart you actually use; chart repositories change over time and publishers sometimes retire versions, so check a current version before relying on one. The important shape is that --helm-chart replaces --path, --revision is the chart version, and --dest-name in-cluster uses the cluster's registered name in place of its address. See the Helm guide for writing charts.
--insecure-oci-force-http when you add the repository. Also, the old spec.source.helm.version: v3 setting is now ignored, because Helm 4 is always used. Real registries use HTTPS, so most people never see this.
- Create a GitHub repository with the two files above and push them.
- Create and sync an Argo CD application pointing at
app/. - Change
replicasto 3 in Git, push, and refresh. - Confirm
OutOfSync, read the diff, sync, and count the pods.
Automated sync: prune and self-heal
So far every change needed a manual sync. That is the safest starting point, but it is also the thing GitOps tries to remove: a human pressing a button is one more step that can be skipped or forgotten. Automated sync tells Argo CD to apply differences by itself.
argocd app set hello --sync-policy automated
Or in the YAML:
spec:
syncPolicy:
automated: {}
With that set, pushing a commit leads, within the polling interval, to Argo CD syncing the change without anyone pressing anything. Two further switches refine the behaviour, and both are off by default even after automation is on. The defaults are cautious because both can be destructive.
Prune controls what happens to things that disappear from Git. Suppose you delete service.yaml from the repository. By default, even with automation, Argo CD will not delete the live Service; it will mark the application OutOfSync and leave the object running. The reasoning is that deleting things is riskier than creating them, so you must opt in. With prune on, removing a file from Git removes the resource from the cluster:
argocd app set hello --auto-prune
Self-heal controls what happens when the live state changes without a Git change. Without it, if someone runs kubectl scale deployment hello --replicas=10, the application becomes OutOfSync and stays that way, waiting for a human. With self-heal, Argo CD notices the drift and reverts it:
argocd app set hello --self-heal
The two together are the usual configuration for a fully GitOps application:
spec:
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Watch self-heal work. With it enabled, scale the deployment by hand and look immediately afterwards:
kubectl scale deployment hello -n hello --replicas=10
kubectl get pods -n hello -w
For a moment there are ten pods, then Argo CD notices and scales back to what Git says, terminating the extras. The manual change is gone without a trace except in events. This is the feature that surprises people during incidents: an engineer hand-edits a production object to fix an outage and watches the fix vanish. The lesson is not to turn self-heal off; it is that the emergency change has to go into Git (or be made by pausing automation first), which is exactly the discipline GitOps exists to enforce.
prune only after you understand what the application owns. A mistaken rename in Git (the old name disappears, the new one appears) looks like a delete and a create, and prune will delete the old object, including, for example, a PersistentVolumeClaim and its data if the claim is in the repository. Test with a dry run first: argocd app sync hello --prune --dry-run.
There is one more rule worth memorising now. Rollback is not allowed while automated sync is on, because the automation would immediately undo the rollback by re-syncing to Git. To roll back an automated application you revert the commit in Git, which is the GitOps way anyway, or temporarily disable automation. The rollback section shows both.
Automated sync can also retry failures. A sync that fails on a transient error (an API timeout, say) can be retried with backoff:
spec:
syncPolicy:
automated:
prune: true
selfHeal: true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
limit is the number of attempts (-1 means retry forever, which is rarely wise), and the backoff block makes each wait longer than the previous one, so a struggling cluster is not hammered.
- Enable automated sync, prune and self-heal on your
helloapplication. - Scale the deployment by hand with
kubectl scaleand watch it revert. - Delete
service.yamlfrom Git, push, refresh, and confirm the Service is pruned.
The everyday commands, grouped by what you are trying to do
You now know enough to use most of the argocd app commands. Grouped by intent, rather than alphabetically, this is the set to keep at hand.
See what exists. List applications and inspect one:
argocd app list
argocd app list -l team=payments
argocd app get hello
argocd app get hello -o tree
argocd app get hello --refresh
argocd app get hello --hard-refresh
-o tree prints the resource tree in the terminal, -o wide adds columns, and -o yaml or -o json gives machine-readable output. -l key=value filters by label. --refresh asks Argo CD to re-compare now; --hard-refresh also clears its cached manifests.
See what would change. Before applying, always be able to answer "what will this do?":
argocd app diff hello
argocd app manifests hello
argocd app sync hello --dry-run
app diff compares live and target. app manifests prints the rendered YAML Argo CD derived from Git, which is how you confirm what a Helm chart actually produced. --dry-run previews a sync without applying it.
Apply changes. Sync everything, or just part of an application:
argocd app sync hello
argocd app sync hello --prune
argocd app sync hello --resource apps:Deployment:hello
argocd app sync hello --revision v1.2.0
argocd app sync hello --async
--resource GROUP:KIND:NAME syncs a single object (an empty group, for core kinds, is written :Service:hello). --revision syncs a specific branch, tag or commit instead of the tracked one, which is useful for testing a branch. --async returns immediately instead of waiting for the sync to finish.
Wait for a result. Scripts need a blocking command:
argocd app wait hello --health --sync --timeout 300
It returns once the application is healthy and synced, or fails at the timeout. CI jobs that test a deployment use it.
Read history and logs:
argocd app history hello
argocd app logs hello --kind Deployment --name hello
argocd app logs hello --kind Deployment --name hello -f
-f follows the log stream, like tail -f.
Change settings on an existing application:
argocd app set hello --sync-policy automated
argocd app set hello --sync-policy none
argocd app set hello --revision main
argocd app set hello -p image.tag=v2
The last form overrides a Helm parameter directly. Careful: settings changed with app set are stored in the Application object in the cluster, so if the Application is itself defined in a file in Git, the file and the cluster now disagree. Prefer editing the file.
Remove an application:
argocd app delete hello
argocd app delete hello --cascade=false
The default deletion cascades and removes the deployed resources; --cascade=false deletes only the Application record and leaves the resources running.
A few global flags apply to every command. --server names the server if you are not using your saved login. --grpc-web helps when a proxy in front of Argo CD does not pass gRPC traffic. --insecure skips certificate checks (never in production). -o picks the output format. Tab completion makes all of this easier, and the CLI generates it for your shell:
argocd completion zsh > ~/.argocd-completion.zsh
Replace zsh with bash, fish or powershell, and source the file from your shell's startup file.
argocd app diff NAME costs two seconds and shows exactly what a sync will do. Its exit code is 0 for no difference, 1 for a difference and 2 for an error, so it also works in scripts: "if there is a diff, tell me".
- Run
argocd app get hello -o treeand compare it with the UI tree. - Run
argocd app manifests helloand find your Deployment in the output. - Run
argocd app wait hello --health --sync --timeout 120and note that it returns immediately when all is well.
Sync history and rolling back
Every successful sync is recorded. The history shows which Git revision was deployed, when, and by whom:
argocd app history hello
ID DATE REVISION
0 2026-10-01 09:41:12 +0300 +03 a1b2c3d
1 2026-10-01 10:05:47 +0300 +03 e4f5a6b
2 2026-10-01 10:30:03 +0300 +03 9c8d7e6
Each row is a numbered deployment. To go back to an earlier one, give its ID:
argocd app rollback hello 1
This re-deploys the manifests that revision produced. There is an important catch, which is the rule from the automated sync section: the command is refused if automated sync is enabled. The rollback would change the cluster away from Git, and automation would immediately change it back. The error reads along the lines of "rollback cannot be initiated when auto-sync is enabled".
It gets more subtle after that. Even when a rollback works on a manual application, it only moves the cluster; Git still contains the bad commit. The application will show OutOfSync because it no longer matches the tracked branch. A rollback is therefore a temporary measure that buys time during an incident, and it is not a fix. The real fix, and the fix for automated applications, is to revert the bad commit in Git:
git revert HEAD
git push
Argo CD then syncs the reverted state like any other change. Because the change is in Git, the history stays honest: the repository records that the change was made and undone, and who did each.
This is a good moment to appreciate what GitOps gives you over kubectl. With hand-applied changes, "what was running yesterday?" has no reliable answer. Here the answer is a revision hash in the history, and a git show of that hash.
spec.revisionHistoryLimit). Git is the real long-term history; Argo CD's list is for quick rollbacks.
- Turn automated sync off with
argocd app set hello --sync-policy none. - Push a change that sets the image to a tag that does not exist, such as
nginx:9.9.9, and sync. Watch the pods fail and the application becomeDegraded. - Run
argocd app history helloandargocd app rollback hellowith the previous ID. - Then
git revertthe bad commit and push.
Connecting a private repository
Everything so far used public repositories. Real repositories are private, and Argo CD needs credentials to read them. A missing or wrong credential produces an error that looks alarming to beginners:
rpc error: code = Unknown desc = ... authentication required
or in the UI, a repository not found warning on the application. The fix is to register the repository with Argo CD along with a credential.
The simplest credential for HTTPS is a personal access token. On GitHub, create a fine-grained token with read-only access to the contents of just that repository, then register it:
argocd repo add https://github.com/YOUR-ORG/private-repo.git \
--username your-github-user \
--password ghp_yourTokenHere
For SSH you use a private key instead:
argocd repo add git@github.com:YOUR-ORG/private-repo.git \
--ssh-private-key-path ~/.ssh/id_ed25519
Check that it connected:
argocd repo list
The STATUS column should say Successful. If it says Failed, the message beside it is the cause, usually a wrong token, insufficient token scope or a typo in the URL.
What argocd repo add actually does is worth understanding: it stores the credential as a Kubernetes Secret in the argocd namespace, labelled argocd.argoproj.io/secret-type: repository. So you can also do it declaratively, and it is how a GitOps setup keeps repository definitions in code (taking care never to commit a real token; secrets management is a mid-level topic).
apiVersion: v1
kind: Secret
metadata:
name: private-repo
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: https://github.com/YOUR-ORG/private-repo.git
username: your-github-user
password: REPLACE_WITH_TOKEN
Never commit that file with a real token in it. Keep the secret out of Git, or use one of the secret-management approaches covered later in this series. A token pasted into a repository is a leaked token.
Two more points. First, SSH connections are checked against known host keys. Since Argo CD 3.5, a repository without configured credentials also validates the host key against the argocd-ssh-known-hosts-cm ConfigMap, so a knownhosts: key mismatch error means the server's key is not registered; add it with argocd cert add-ssh --batch. Second, for Helm or OCI registries, add --type helm or --type oci to argocd repo add, plus a --name.
--password TOKEN on the command line stores the token in your shell history and shows it in the process list while the command runs. For anything beyond a personal sandbox, use a declarative Secret created by your secrets tooling, or an SSH deploy key, and revoke any token you have ever pasted into a terminal in front of others.
- Make your test repository private.
- Run
argocd app get hello --hard-refreshand read the error. - Create a read-only token, register the repository with
argocd repo add, and runargocd repo list. - Refresh the application again.
Projects: the rules an application plays by
Every Application belongs to an AppProject. Until now everything used default, which allows any repository, any cluster and any namespace. That is convenient for learning and dangerous for sharing, so in a team setting each group gets its own project with restrictions. As a beginner you will mostly meet projects through their error messages, so this section teaches you to read them.
A project can restrict four main things: which source repositories its applications may use (sourceRepos), which destinations they may deploy to (destinations, pairs of cluster and namespace), which cluster-wide kinds of object they may create (cluster-scoped resources such as Namespaces are denied unless whitelisted), and which namespaced kinds are blocked.
Here is a small project:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-a
namespace: argocd
spec:
description: Applications owned by team A
sourceRepos:
- https://github.com/YOUR-ORG/team-a-*
destinations:
- server: https://kubernetes.default.svc
namespace: team-a-*
clusterResourceWhitelist: []
Apply it with kubectl apply -f team-a-project.yaml, then create an Application with --project team-a. If the application strays outside the rules, Argo CD refuses with a message that names the exact rule:
application repo https://github.com/someone/else.git is not permitted in project 'team-a'
application destination server 'https://kubernetes.default.svc' and namespace 'prod' do not match any of the allowed destinations in project 'team-a'
resource :Namespace is not permitted in project team-a
The first means the repository is not in sourceRepos. The second means the destination is not in destinations. The third means the repository contains a cluster-scoped object (here a Namespace) that the project does not allow. Each has a matching fix, done either by editing the project file or with argocd proj add-source, argocd proj add-destination or argocd proj allow-cluster-resource. The key reading skill is that these messages are not bugs; they are the project doing its job, and the answer is to decide whether the rule or the application is wrong.
A related surprise: the CreateNamespace=true option creates a Namespace, but a project that blocks cluster-scoped kinds can still allow it because the option is handled by Argo CD rather than by a manifest in the repository. If instead you commit a Namespace object into Git inside a restricted project, you will meet the third error above.
Projects also carry roles, sync windows and more, which the Mid-level guide covers. For now remember that default is unrestricted, that you cannot delete it, and that a platform team usually locks it down.
- Create the
team-aproject above. - Try to create an application in it with a repository that does not match
sourceRepos. - Read the error, then fix it with
argocd proj add-source team-a URL.
Deleting applications safely
Deleting is where the finalizer from earlier earns its place. There are two separate things you might want to delete: the Application (Argo CD's record) and the resources it deployed (your Deployments and Services). Which happens depends on a setting.
With the finalizer resources-finalizer.argocd.argoproj.io on the Application, deletion is cascading: Argo CD deletes the deployed resources first, and then removes the Application. This is what people usually expect, and what argocd app delete does by default on an application created through the CLI.
Without the finalizer, deleting the Application object (for example with kubectl delete application hello -n argocd) removes only the record. Your resources keep running, and nothing manages them any more. That can be exactly what you want when you are handing resources over, and it can be a surprise that leaves forgotten workloads running and costing money.
argocd app delete hello
argocd app delete hello --cascade=false
Two rules keep you safe. First, put the finalizer in every Application you write as a file, unless you have a reason not to. Second, never delete a production application without first reading what is in its resource tree: cascading delete removes whatever the tree shows, which can include persistent volume claims.
Deleting an application from a parent "app of apps" (an advanced pattern where one Application creates others) cascades too, which can take out many applications at once. That is one of the reasons the mid-level guide spends time on prune protection.
Also remember that with automated sync and prune on, deleting a file from Git is itself a deletion. The destructive power is the same; it just arrives through a commit.
metadata.finalizers before you delete anything important.
- Delete your
helloapplication with--cascade=falseand confirm the pods survive withkubectl get pods -n hello. - Recreate the application and sync it so Argo CD adopts the existing objects.
- Now delete it normally and confirm the pods are removed.
How Argo CD notices changes: polling, refresh and webhooks
Newcomers often ask why Argo CD did not react the instant they pushed. The answer is the polling behaviour introduced earlier, and knowing the options makes the tool feel predictable.
By default, Argo CD re-checks each application's Git repository every 120 seconds plus up to 60 seconds of jitter, so "about three minutes" at the outside. The interval is the timeout.reconciliation setting in the argocd-cm ConfigMap, with timeout.reconciliation.jitter controlling the random spread. Setting the interval to 0 disables polling and is not recommended.
You have three ways to bring that delay down.
Manual refresh. Clicking Refresh in the UI, or running argocd app get NAME --refresh, makes Argo CD check Git immediately for that application. Use --hard-refresh when you suspect a stale cache: it also discards the repo server's saved rendering.
Webhooks. Your Git host can notify Argo CD when a push happens, and Argo CD refreshes the affected applications straight away. The receiver is at /api/webhook on your Argo CD address, and it supports GitHub, GitLab, Bitbucket, Bitbucket Server, Gogs and Azure DevOps (and, since 3.5, GitHub Container Registry). You add the webhook in your Git host pointing at https://YOUR-ARGOCD/api/webhook and, if you set a shared secret, store the same secret in Argo CD's argocd-secret. This is an administrator task that needs a reachable address, so a laptop cluster behind a port-forward generally cannot receive webhooks.
Annotation. You can trigger a refresh of an Application by annotating it, which is how automation does it:
kubectl annotate application hello -n argocd argocd.argoproj.io/refresh=normal --overwrite
Use hard instead of normal for a hard refresh.
It helps to keep straight that refresh (compare) and sync (apply) are different things. A refresh only updates the status. If automated sync is off, a refresh will show OutOfSync and change nothing in the cluster. If automated sync is on, the detected difference is then applied by the controller without you pressing anything.
# argocd-cm excerpt (administrator setting)
data:
timeout.reconciliation: 180s
timeout.reconciliation.jitter: 60s
Changing these values is an operations matter, and lowering the interval raises load on both Argo CD and your Git host, so prefer webhooks over aggressive polling.
- Push a small change and time how long the UI takes to show
OutOfSyncwithout refreshing. - Push another and press Refresh immediately.
- Compare the two timings.
Configuration you will meet as a beginner
Argo CD is configured through Kubernetes ConfigMaps and Secrets in its own namespace, each with a fixed name. You do not need to edit them yet, but you should know they exist and what lives where, because documentation and error messages refer to them constantly.
| Object | What it holds |
|---|---|
argocd-cm |
General settings: the external URL, polling interval, single sign-on, custom health checks, resource exclusions |
argocd-cmd-params-cm |
Flags for the components, such as whether the server runs without TLS |
argocd-rbac-cm |
Who may do what (the permission policy) |
argocd-secret |
The admin password hash, signing key, webhook secrets |
argocd-ssh-known-hosts-cm |
Trusted SSH host keys for Git servers |
argocd-tls-certs-cm |
Extra certificate authorities for HTTPS Git servers |
Two practical habits. The first: after editing argocd-cmd-params-cm, restart the affected component, because those values are read at start-up. For example, after changing the server's settings:
kubectl rollout restart deployment argocd-server -n argocd
The second: a few settings are used so often that beginners hit them early. url in argocd-cm is required for single sign-on and for links in notifications. Setting server.insecure: "true" in argocd-cmd-params-cm makes the API server serve plain HTTP, which is what you want when TLS is terminated by an ingress in front of it (and never when it is exposed directly). admin.enabled: "false" in argocd-cm switches off the built-in admin account once real accounts exist.
You can check a configuration without applying it, using the administrator command:
argocd admin settings validate -n argocd
It reads the settings from the cluster and reports problems. It needs direct access to Kubernetes through your kubeconfig, which a cluster administrator has.
Finally, you can read what is deployed from any ConfigMap with kubectl get cm -n argocd argocd-cm -o yaml. Get in the habit of looking at real configuration rather than trusting a tutorial's description; since 3.0 several defaults changed (for instance, Argo CD now tracks the resources it owns with an annotation named argocd.argoproj.io/tracking-id instead of a label, and logs require an explicit permission), and older articles describe the 2.x behaviour.
- Run
kubectl get cm -n argocdand match each name to the table above. - Run
kubectl get cm argocd-cm -n argocd -o yamland read what a default installation contains. - Run
argocd admin settings validate -n argocd.
Common errors, and how to read them
Most Argo CD failures fall into a handful of families. The skill is to read the message, decide which component produced it, and look in the right place. Here are the ones beginners meet, with what each really means.
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long. You installed or upgraded with a plain kubectl apply. Re-run with --server-side --force-conflicts.
x509: certificate signed by unknown authority (from the CLI). The server uses its default self-signed certificate. For a local experiment add --insecure; for anything real, install a proper certificate.
rpc error: code = Unavailable desc = transport is closing. A proxy or ingress between you and the server does not carry gRPC. Add --grpc-web to the CLI command.
repository not found, or authentication errors on an application. The credential is missing, wrong, or scoped too narrowly, or the URL has a typo. Register the repository with argocd repo add and check argocd repo list for Successful.
app path does not exist. The path in the Application is wrong for that revision. Check the folder name and the targetRevision; a branch that does not contain the folder produces this message, and so does a capital-letter mismatch.
unable to resolve '<revision>' to a commit SHA. The branch or tag in targetRevision does not exist or is not readable. Fix the name, or the credentials.
Manifest generation error (cached). The repo server tried to render your manifests, failed, and cached the failure so it does not retry constantly. Read the repo server logs for the real cause, fix the source, and then run argocd app get NAME --hard-refresh to clear the cached error.
application repo ... is not permitted in project, application destination ... do not match any of the allowed destinations, and resource ... is not permitted in project. The project's rules are refusing the application, as described in the projects section.
there are no clusters with this name. A destination.name does not match any registered cluster. Run argocd cluster list and copy the name exactly, or use server.
existing application spec is different, use upsert flag to force update. You ran argocd app create for a name that exists with different settings. Use --upsert if you mean to overwrite, or use argocd app set.
Sync fails with namespace ... not found. The destination namespace does not exist. Add CreateNamespace=true to the sync options.
The application is permanently OutOfSync right after a successful sync. Something in the cluster keeps changing a field that your manifest also sets: a mutating webhook, a controller that adds defaults, or another tool. Open the Diff tab on the resource to see which field differs. The fix, usually an ignoreDifferences rule, is covered at the Mid-level; as a beginner your job is to find the field.
The application is stuck in Progressing. Argo CD is waiting for a health check that never finishes. A Service of type LoadBalancer or an Ingress with no address yet is a common cause on local clusters that cannot allocate one. Read the resource's message in the tree.
You cannot see the Logs tab. Since Argo CD 3.0, viewing logs needs its own explicit permission (logs, get). That is an RBAC matter, and it confuses people who used 2.x.
Sync operation blocked by sync window. An administrator has defined a time window that denies syncs right now. Wait, or ask whether manual syncs are permitted during the window.
A forgotten admin password. The initial secret may already be deleted. The administrator reset procedure generates a bcrypt hash with argocd account bcrypt --password NEW and stores it in argocd-secret; do that only on a cluster you own, and follow the official documentation for the exact keys.
A general-purpose debugging order, which works for almost everything, goes like this. First argocd app get NAME for the statuses and the message. Second, argocd app diff NAME for the difference. Third, the repo server logs if the problem smells like rendering, Git or credentials, and the controller logs if it smells like comparing or applying. Fourth, kubectl describe on the failing resource, because Kubernetes events often explain a stuck pod better than Argo CD can. Finally, try a hard refresh before concluding that a problem is real, since a stale cache explains a surprising number of ghosts.
argocd app get hello
argocd app diff hello
kubectl logs -n argocd deploy/argocd-repo-server --tail=50
kubectl logs -n argocd statefulset/argocd-application-controller --tail=50
kubectl describe deploy hello -n hello
argocd app get hello --hard-refresh
- Break an application on purpose: set
pathto a folder that does not exist. - Read the error in
argocd app getand in the UI. - Fix the path and run a hard refresh.
- Then set the image to a non-existent tag and find the cause in
kubectl describe pod.
Putting it all together
Here is one small end-to-end project that uses everything above. You will run a web application whose configuration lives in Git, with Argo CD deploying and healing it, and you will change it only by committing.
Step 1: the repository. Create a public repository argo-demo with this layout:
argo-demo/
apps/
hello/
deployment.yaml
service.yaml
argocd/
hello-app.yaml
The apps/hello files are the Deployment and Service from earlier (two replicas of nginx:1.27). The argocd/hello-app.yaml file is the Application definition itself, stored next to what it deploys:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hello
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/YOUR-USER/argo-demo.git
targetRevision: main
path: apps/hello
destination:
server: https://kubernetes.default.svc
namespace: hello
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Step 2: bootstrap once. The Application is the only thing you apply by hand, and only once. Everything afterwards flows from Git:
kubectl apply -f argocd/hello-app.yaml
argocd app wait hello --health --sync --timeout 180
kubectl get pods -n hello
Within a minute the application is Synced and Healthy with two pods. You have not run a sync command; automation did it.
Step 3: change through Git. Edit deployment.yaml to set replicas: 3 and the image to nginx:1.27.1. Commit with a meaningful message, push, and watch:
git commit -am "Scale hello to 3 and bump nginx" && git push
argocd app get hello --refresh
argocd app history hello
The application rolls forward, and the history gains an entry pointing at your commit.
Step 4: prove drift is corrected. Hand-edit the live object, then watch it revert:
kubectl scale deployment hello -n hello --replicas=1
sleep 15
kubectl get deploy hello -n hello
Self-heal restores three replicas.
Step 5: break it, then recover through Git. Push an image tag that does not exist. The application goes Degraded (pods in ImagePullBackOff), while the status stays Synced: Git is wrong, and the cluster faithfully matches it. Fix it with:
git revert HEAD --no-edit && git push
Argo CD syncs the revert and health returns.
Step 6: prune. Delete service.yaml from Git, push, and confirm that kubectl get svc -n hello no longer lists it, because prune is on. Restore it with another commit.
Step 7: clean up.
argocd app delete hello
kind delete cluster --name argo-demo
The finalizer removes the deployed resources before the Application goes away.
You have now done the full cycle: bootstrap, deploy, change, heal, break, recover, prune and delete. Notice that after step 2, the only write path into the cluster was a Git commit. That constraint is the point.
- Build the repository and run steps 1 to 7 end to end.
- For each step, write down which component did the work: your commit, Argo CD's repo server, its controller, or Kubernetes.
What you can now do, and what comes next
If you worked through the Try it tasks, you can now do the following, and can explain it to someone else:
- Explain why pull-based delivery removes the need for cluster credentials in CI, and what GitOps means.
- Name the core nouns (Application, Project, sync status, health, target and live state) and the three components (server, repo server, controller).
- Install Argo CD with the current server-side command, install the CLI, log in, and change the initial password.
- Create an Application from the CLI and from YAML, and read its status, diff and resource tree.
- Deploy your own repository, and change a cluster only by committing.
- Choose between manual and automated sync, and explain prune and self-heal.
- Roll back an incident correctly, and know why a revert in Git is the real fix.
- Connect a private repository, read project errors, and delete applications without losing data by accident.
- Diagnose the common errors by reading the message and choosing the right component to inspect.
The Mid-level guide picks up with how Argo CD decides what is different (diffing and resource tracking), projects and role-based access in depth, single sign-on, ApplicationSets that generate many applications from a template, the app-of-apps pattern, sync waves and hooks for ordering, sync windows, notifications, and the integration with CI pipelines. The Senior guide covers running Argo CD as a platform: high availability, scaling, multi-cluster management, security and upgrade strategy.
For neighbouring topics in this catalogue, Kubernetes is the foundation everything here sits on, Helm and Kustomize are the tools you will most often point Argo CD at, Terraform creates the clusters Argo CD runs in, and Flux is the alternative GitOps controller worth comparing once you are comfortable here.
One regional note for readers deploying in the Gulf and Egypt. Data-residency rules often dictate which cloud region your clusters run in. Because Argo CD pulls from Git and needs no inbound credentials in CI, the control plane can sit in the same in-country region as your workloads, and only Git traffic crosses the boundary. Decide where your repositories are hosted with the same care as where your clusters are, since the repository contents (including any configuration values) are data too.