
You’ve written your Kubernetes manifests, pushed them to Git, and now you need a reliable way to keep your cluster in sync with what’s in the repo. Manually running kubectl apply works until it doesn’t — a missed apply, a direct cluster edit, or a hotfix that never made it back to Git, and your cluster is in an unknown state.
ArgoCD solves this with a simple guarantee: your cluster should look exactly like what’s in Git. If it doesn’t, ArgoCD tells you — and can automatically fix it. This is the core of GitOps, and ArgoCD is the most widely adopted tool for implementing it on Kubernetes.
This guide walks you through a complete ArgoCD installation on Kubernetes, deploying your first application, and the configuration that actually matters in production.
What Is ArgoCD?
ArgoCD is a declarative GitOps continuous delivery tool for Kubernetes. It runs inside your cluster, watches one or more Git repositories, and ensures the live cluster state matches what’s defined in those repos.
If you want to understand the GitOps model and how ArgoCD compares to Flux before going hands-on, start with our ArgoCD overview and the Flux vs ArgoCD comparison. This guide assumes you’ve made the call to go with ArgoCD and want to get it running.
How it works in one sentence: ArgoCD polls your Git repo (or receives a webhook), compares the desired state in Git against the live state in the cluster, and either alerts you to drift or automatically syncs the difference.
Prerequisites
Before you start:
- A running Kubernetes cluster (local with kind/minikube, or managed — EKS, AKS, GKE, DigitalOcean Kubernetes all work)
kubectlconfigured and pointing at your clustergitinstalled locally- A Git repository to use as your GitOps source (GitHub, GitLab, Bitbucket all work)
If you’re new to the core Kubernetes tooling, our Kubernetes commands reference covers the kubectl basics you’ll need throughout this guide.
Step 1 — Install ArgoCD
ArgoCD runs in its own namespace. Create it and apply the install manifest:
# Create the argocd namespace
kubectl create namespace argocd
# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
Wait for all pods to be running:
kubectl wait --for=condition=available --timeout=300s deployment/argocd-server -n argocd
Verify everything is up:
kubectl get pods -n argocd
You should see these pods in Running state:
argocd-application-controller-0
argocd-applicationset-controller-xxx
argocd-dex-server-xxx
argocd-notifications-controller-xxx
argocd-redis-xxx
argocd-repo-server-xxx
argocd-server-xxx
Step 2 — Access the ArgoCD UI
By default, the ArgoCD server service is of type ClusterIP — not externally accessible. For initial setup, use port-forwarding:
kubectl port-forward svc/argocd-server -n argocd 8080:443
Open https://localhost:8080 in your browser. You’ll get a certificate warning — this is expected with the default self-signed cert. Proceed anyway for now.
Get the initial admin password:
kubectl get secret argocd-initial-admin-secret \
-n argocd \
-o jsonpath="{.data.password}" | base64 -d && echo
Log in with username admin and the password from the command above.
Change the admin password immediately after first login — go to User Info → Update Password in the UI, or via CLI:
argocd account update-password \
--current-password <initial-password> \
--new-password <your-new-password>
Step 3 — Install the ArgoCD CLI
The CLI isn’t strictly required, but it’s far faster for most operations than the UI:
# macOS
brew install argocd
# Linux
curl -sSL -o argocd-linux-amd64 \
https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64
# Verify
argocd version
Log in via CLI (while port-forward is running):
argocd login localhost:8080 \
--username admin \
--password <your-password> \
--insecure
Step 4 — Deploy Your First Application
ArgoCD uses an Application custom resource to define what to deploy and where to get it from. You can create this via UI, CLI, or YAML — YAML is the GitOps-native way.
Option A — Via YAML (recommended)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd # Application resource lives in argocd namespace
spec:
project: default
source:
repoURL: https://github.com/your-org/your-k8s-manifests.git
targetRevision: HEAD # Track the default branch
path: apps/my-app # Path inside the repo where manifests live
destination:
server: https://kubernetes.default.svc # Deploy to the same cluster ArgoCD runs in
namespace: production
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Auto-fix drift if someone edits the cluster directly
syncOptions:
- CreateNamespace=true # Create the destination namespace if it doesn't exist
Apply it:
kubectl apply -f my-app.yaml
Option B — Via CLI
argocd app create my-app \
--repo https://github.com/your-org/your-k8s-manifests.git \
--path apps/my-app \
--dest-server https://kubernetes.default.svc \
--dest-namespace production \
--sync-policy automated \
--auto-prune \
--self-heal
Check the application status
# Via CLI
argocd app get my-app
# Via kubectl
kubectl get application my-app -n argocd
The STATUS field will show one of:
Synced— cluster matches GitOutOfSync— cluster has drifted from GitUnknown— ArgoCD can’t determine the state
Step 5 — Connect a Private Repository
If your manifests repo is private, ArgoCD needs credentials. For GitHub, the recommended approach is an SSH key or a GitHub App — avoid using personal access tokens in production.
SSH key method:
# Generate a deploy key (no passphrase)
ssh-keygen -t ed25519 -C "argocd@yourcluster" -f ~/.ssh/argocd_deploy_key -N ""
# Add the public key to your GitHub repo
# GitHub repo → Settings → Deploy keys → Add deploy key
cat ~/.ssh/argocd_deploy_key.pub
# Register the private key with ArgoCD
argocd repo add git@github.com:your-org/your-k8s-manifests.git \
--ssh-private-key-path ~/.ssh/argocd_deploy_key
Verify the repo is connected:
argocd repo list
Step 6 — Sync Policies Explained
This is where most teams get configuration wrong. Here’s what each sync policy option actually does:
syncPolicy:
automated:
prune: true # IMPORTANT: without this, deleted resources in Git stay in the cluster
selfHeal: true # Without this, manual kubectl edits won't be reverted
syncOptions:
- CreateNamespace=true # Auto-create target namespace
- PrunePropagationPolicy=foreground # Wait for dependent resources before pruning
- ApplyOutOfSyncOnly=true # Only apply resources that have actually changed (faster)
- ServerSideApply=true # Use server-side apply (better for large resources)
retry:
limit: 5 # Retry failed syncs up to 5 times
backoff:
duration: 5s
factor: 2
maxDuration: 3m
The prune flag is the one that surprises people most. Without it, if you delete a Deployment from Git, it keeps running in the cluster. ArgoCD considers this “OutOfSync” but won’t remove it. In most setups you want prune: true — just make sure your team understands that removing something from Git means removing it from the cluster.
selfHeal: true is critical for a true GitOps workflow. Without it, someone can kubectl edit a resource directly and the change sticks. With it, ArgoCD reverts any out-of-band changes within the sync interval (default 3 minutes).
Step 7 — RBAC for ArgoCD
ArgoCD has its own RBAC system on top of Kubernetes RBAC. By default, all authenticated users get read-only access. You configure project-level and cluster-level permissions via the argocd-rbac-cm ConfigMap.
Before configuring ArgoCD RBAC, make sure your underlying Kubernetes RBAC is properly scoped — ArgoCD’s service account needs specific cluster permissions and those should follow least-privilege principles.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:readonly # Default role for all authenticated users
policy.csv: |
# Admins — full access
p, role:admin, applications, *, */*, allow
p, role:admin, clusters, get, *, allow
p, role:admin, repositories, *, *, allow
# Developers — deploy only to staging, read everywhere
p, role:developer, applications, get, */*, allow
p, role:developer, applications, sync, */staging-*, allow
p, role:developer, applications, action/*, */staging-*, allow
# Bind SSO groups to roles
g, your-org:platform-team, role:admin
g, your-org:dev-team, role:developer
Step 8 — Expose ArgoCD Properly (Production)
Port-forwarding is fine for local access. In production you need a proper ingress. Here’s an example using the NGINX ingress controller:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-ingress
namespace: argocd
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
ingressClassName: nginx
rules:
- host: argocd.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
tls:
- hosts:
- argocd.yourdomain.com
secretName: argocd-tls
Note ssl-passthrough: "true" — ArgoCD server terminates TLS itself, so you pass the TLS connection through the ingress rather than terminating at the ingress controller.
To lock down who can reach the ArgoCD UI at the network level, pair this with Kubernetes Network Policies restricting ingress to the argocd namespace from your ingress controller only.
Step 9 — ApplicationSets for Multi-Environment Deployments
Manually creating one Application per environment gets tedious fast. ApplicationSet generates multiple Application resources from a template — one definition to manage dev, staging, and production.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app-environments
namespace: argocd
spec:
generators:
- list:
elements:
- env: dev
namespace: development
revision: develop
- env: staging
namespace: staging
revision: main
- env: production
namespace: production
revision: main
template:
metadata:
name: "my-app-{{env}}"
spec:
project: default
source:
repoURL: https://github.com/your-org/your-k8s-manifests.git
targetRevision: "{{revision}}"
path: "apps/my-app/{{env}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{namespace}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
This creates three ArgoCD Applications — my-app-dev, my-app-staging, my-app-production — all managed from a single ApplicationSet definition.
Common Mistakes and How to Avoid Them
1. Not enabling prune: true Resources deleted from Git keep running in the cluster. Your Git repo and cluster drift silently over time. Always set prune: true unless you have a specific reason not to.
2. Storing secrets directly in Git ArgoCD syncs everything in your repo to the cluster — including Kubernetes Secret manifests if you commit them. Never commit raw secrets to Git. Use Sealed Secrets, External Secrets Operator, or HashiCorp Vault. This is one of the most critical security mistakes in GitOps setups.
3. Using HEAD tracking on production Tracking HEAD means every merge to your main branch immediately syncs to production. For production, pin to a specific tag or use a separate branch with a manual promotion step:
targetRevision: v1.4.2 # Pin to a release tag for production
4. One repo for everything Putting application code and Kubernetes manifests in the same repo creates noise — every code commit triggers an ArgoCD sync check. Separate your app code repo from your GitOps manifests repo.
5. Skipping health checks ArgoCD has built-in health checks for standard resources (Deployment, StatefulSet, etc.). For custom resources, you need to define custom health check scripts in argocd-cm. Without them, ArgoCD reports Healthy even when your application is broken.
Best Practices
- Use Projects to group related Applications and set RBAC boundaries between teams
- Enable notifications — ArgoCD Notifications can post to Slack, Teams, or PagerDuty on sync failures, health degradation, and successful deployments
- Use
argocd app diffbefore manual syncs to see exactly what will change - Set resource exclusions for resources you don’t want ArgoCD to manage (e.g., certain ConfigMaps managed by operators)
- Monitor ArgoCD itself — scrape
argocd-metricsandargocd-server-metricswith Prometheus. Key metrics:argocd_app_info,argocd_app_sync_total,argocd_app_health_status - Back up ArgoCD state — all Application definitions should live in Git themselves (App of Apps pattern), so you can recover a wiped ArgoCD installation by re-applying your app definitions
What’s Next
With ArgoCD running and your first application deployed, the natural next steps in the GitOps cluster are:
- GitHub Actions + Kubernetes — wire your CI pipeline to update image tags in the manifests repo, triggering ArgoCD to sync automatically. Check out our introduction to GitHub Actions if you need the CI fundamentals first.
- Helm with ArgoCD — deploying Helm charts via ArgoCD, overriding values per environment
- ArgoCD Image Updater — automatically update image tags in Git when a new image is pushed to your registry
- Notifications — configuring Slack/Teams alerts for sync failures and deployment events
If you’re evaluating whether to use ArgoCD or Flux for your GitOps setup, our Flux vs ArgoCD comparison breaks down the trade-offs in detail.
GitOps fundamentally changes how your team interacts with the cluster — instead of running kubectl apply, you make a PR. That shift in workflow takes a week to get used to and pays back for years.