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:

  1. Clones the repo, finds guestbook/
  2. Renders the manifests there (plain YAML in this case; Argo also handles Helm and Kustomize automatically)
  3. Creates the hello namespace, applies the resources
  4. 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-plugin or kustomize-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=merge is 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/webhook for near-instant sync.
  • Large clusters under one Argo — the application controller becomes the bottleneck. Shard apps across multiple controllers with the argocd.argoproj.io/instance label.