Install
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods
kubectl -n argocd get pods -w
Five pods come up: argocd-server, argocd-repo-server, argocd-application-controller, argocd-redis, argocd-applicationset-controller (plus Dex if SSO is enabled).
Get the initial admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d ; echo
Port-forward the UI:
kubectl -n argocd port-forward svc/argocd-server 8080:443
Browse to https://localhost:8080 (self-signed cert; accept), log in as admin with that password. Change the password immediately — User Info → Update Password.
Argo CD CLI
curl -sSL -o /usr/local/bin/argocd \
https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x /usr/local/bin/argocd
argocd login localhost:8080 --insecure
# Username: admin
# Password: <the one from above>
Your first Application
An Argo CD Application is a custom resource that points at a Git repo path and a Kubernetes namespace. Create one:
cat <<'EOF' | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hello
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/argoproj/argocd-example-apps.git
targetRevision: HEAD
path: guestbook
destination:
server: https://kubernetes.default.svc
namespace: hello
syncPolicy:
syncOptions:
- CreateNamespace=true
automated:
prune: true
selfHeal: true
EOF
Within a few seconds, Argo CD:
- Clones the repo, finds
guestbook/ - Renders the manifests there (plain YAML in this case; Argo also handles Helm and Kustomize automatically)
- Creates the
hellonamespace, applies the resources - Marks the app Healthy + Synced in the UI
automated.selfHeal: true means any out-of-band kubectl edit on the cluster is reverted to match Git within seconds. automated.prune: true means deleting a resource from Git removes it from the cluster.
App-of-apps pattern
For real fleets, manually creating each Application doesn't scale. The "app of apps" pattern: one Argo CD Application points at a directory of Application manifests; managing the fleet becomes managing that directory in Git.
# Repo: github.com/me/cluster
# Layout:
# apps/
# argocd/ (Argo managing itself)
# authentik/
# immich/
# paperless/
# uptime-kuma/
# bootstrap/
# root.yaml (the one Application pointing at apps/)
bootstrap/root.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/me/cluster.git
targetRevision: HEAD
path: apps
directory: { recurse: true }
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Apply root.yaml once with kubectl apply -f; from that point on every app's lifecycle is a Git commit.
Helm and Kustomize
Argo CD detects what kind of source it's looking at and renders accordingly. For Helm:
spec:
source:
chart: cert-manager
repoURL: https://charts.jetstack.io
targetRevision: v1.15.0
helm:
values: |
installCRDs: true
prometheus:
enabled: true
servicemonitor: { enabled: true }
For Kustomize: any kustomization.yaml in the directory is recognized; the overlay is rendered. Mix and match across Applications in the same cluster.
Sensitive values: External Secrets Operator
Argo CD wants everything in Git, but secrets can't go in plain Git. Two common patterns:
- External Secrets Operator (ESO) — commit a SecretStore + ExternalSecret manifest; ESO fetches the actual value from Vault (see Vault tutorial), AWS Secrets Manager, 1Password, etc. Argo CD manages the references; the secret itself never enters Git.
- SOPS-encrypted manifests +
argocd-vault-pluginorkustomize-sops— the secret is encrypted at rest in Git with age/PGP; an Argo CD plugin decrypts at render time. Useful for small homelabs where running ESO is overkill.
Expose the Argo CD UI
Port-forwarding is fine for solo use. For team access, expose the UI via an Ingress (Traefik in K3s default):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd
namespace: argocd
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
spec:
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port: { number: 443 }
tls:
- hosts: [ argocd.example.com ]
secretName: argocd-tls
For gRPC support (the CLI uses it), set argocd-server's --insecure behind the TLS-terminating ingress, or use the official argocd-server-grpc approach with split ingresses.
SSO with OIDC / Authentik
Edit the argocd-cm ConfigMap:
data:
url: https://argocd.example.com
oidc.config: |
name: Authentik
issuer: https://auth.example.com/application/o/argocd/
clientID: <client-id-from-authentik>
clientSecret: $oidc.authentik.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
requestedIDTokenClaims: { "groups": { "essential": true } }
And map groups to RBAC in argocd-rbac-cm:
data:
policy.default: role:readonly
policy.csv: |
g, argocd-admins, role:admin
g, ops-team, role:admin
g, dev-team, role:<your-custom-readonly-with-restart>
The Authentik tutorial (here) walks through the OIDC-provider side.
What goes wrong
- Drift between Git and cluster after manual changes — the UI shows it as "OutOfSync." With
selfHeal: true, it auto-corrects; without, the next manual sync is needed. - Stuck-finalizer namespaces when an app's controller has a finalizer that won't complete.
kubectl patch namespace stuck -p '{"metadata":{"finalizers":[]}}' --type=mergeis the escape hatch. - Webhook latency — by default Argo polls Git every 3 minutes. Configure a webhook from the Git provider to
https://argocd.example.com/api/webhookfor near-instant sync. - Large clusters under one Argo — the application controller becomes the bottleneck. Shard apps across multiple controllers with the
argocd.argoproj.io/instancelabel.