Fix: ArgoCD Not Working — OutOfSync, Sync Waves, RBAC, Helm/Kustomize, and Webhook Setup
Quick Answer
How to fix ArgoCD errors — application stuck OutOfSync, sync waves not respected, RBAC permission denied, Helm values not merged, ApplicationSet generator config, repo auth, and webhook not triggering.
The Error
You commit a change to your manifests but ArgoCD never picks it up:
NAME SYNC STATUS HEALTH STATUS
my-app OutOfSync HealthyOr auto-sync is enabled but the diff stays:
spec:
syncPolicy:
automated:
prune: true
selfHeal: true# Still OutOfSync after 30 minutes.Or sync waves don’t run in the order you expected:
metadata:
annotations:
argocd.argoproj.io/sync-wave: "1"# Wave 1 resources start AFTER wave 5 finished.Or you grant a user permission but they still get permission denied:
ERROR: code = PermissionDenied desc = permission denied: applications, sync, default/my-appWhy This Happens
ArgoCD compares the Git state to the cluster state and reports diffs. Most issues come from one of:
- Sync polling vs webhook. By default ArgoCD polls Git every 3 minutes. A change you just pushed may not appear for that long. Webhooks bring it down to seconds.
- Auto-sync conditions. Auto-sync runs only when ArgoCD’s sync controller is healthy and there are no manual
--dry-runsyncs in flight and the resource is sync-eligible. Resources excluded viaignoreDifferencesor with a finalizer hold may stay OutOfSync forever. - Sync waves vs hooks. Sync waves group resources by
sync-waveannotation; lower numbers go first. But hooks (PreSync/Sync/PostSync) cross-cut waves — a PreSync hook for wave 5 still runs before wave 1’s main resources. - RBAC is layered. Three levels combine: cluster-level (RoleBindings), project-level (AppProject), and global ArgoCD RBAC (configmap or OIDC group mapping). A “permission denied” can come from any layer.
Fix 1: Check Application Status in Detail
The CLI shows the full picture:
argocd app get my-appOutput includes:
- Sync status with the exact resources OutOfSync.
- Health status with messages for each resource.
- Conditions explaining why sync isn’t progressing.
For diffs:
argocd app diff my-appOr from the UI: Application → DIFF tab. Drift comes in three flavors:
- Expected diffs (changes you intend to apply).
- Drift in the cluster (manual
kubectl edit, controllers mutating resources). - Ignored fields that ArgoCD treats as never-equal but doesn’t surface.
For the third, check spec.ignoreDifferences on the Application and resource.customizations.ignoreDifferences in the ArgoCD ConfigMap.
Pro Tip: Run argocd app sync my-app --dry-run to see what would change without actually applying. Useful for verifying a fix before clicking Sync.
Fix 2: Configure a Webhook Instead of Polling
Polling every 3 minutes is fine for batch updates. For tight feedback loops, set up a Git webhook:
# In GitHub → Settings → Webhooks:
URL: https://argocd.example.com/api/webhook
Content-Type: application/json
Secret: (your webhook secret)
Events: Just the push eventIn ArgoCD’s argocd-secret:
apiVersion: v1
kind: Secret
metadata:
name: argocd-secret
namespace: argocd
data:
webhook.github.secret: <base64-secret>After applying, pushes to Git trigger ArgoCD to re-evaluate immediately. The “sync” itself still respects auto-sync rules; webhooks just remove the poll delay.
Common Mistake: Setting the webhook to “Pull request” events. ArgoCD wants push events to pick up new commits to tracked branches. PR events fire on PR creation but not on the actual push that updated the branch.
Fix 3: Auto-Sync With prune and selfHeal
For full GitOps (cluster matches Git, period), enable auto-sync with both flags:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
destination:
namespace: my-app
server: https://kubernetes.default.svc
project: default
source:
repoURL: https://github.com/myorg/manifests
path: apps/my-app
targetRevision: main
syncPolicy:
automated:
prune: true # Delete resources removed from Git.
selfHeal: true # Revert manual changes in the cluster.
syncOptions:
- CreateNamespace=true
- ApplyOutOfSyncOnly=true
- PrunePropagationPolicy=foregroundThree syncOptions worth knowing:
CreateNamespace=true— ArgoCD createsdestination.namespaceif it doesn’t exist.ApplyOutOfSyncOnly=true— only apply the resources that differ (faster, less churn).PrunePropagationPolicy=foreground— wait for dependents (pods) to delete before removing parents (deployments).
Common Mistake: Setting selfHeal: true without realizing it kills any manual kubectl edit. If you scale a Deployment from the CLI, ArgoCD reverts it on the next sync. Either commit changes to Git, or set ignoreDifferences for spec.replicas on Deployments you want to scale manually:
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicasFix 4: Sync Waves and Hooks
Use sync-wave to order resources within a single sync:
# Database first:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
annotations:
argocd.argoproj.io/sync-wave: "0"
# Then migrations:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/sync-wave: "1"
argocd.argoproj.io/hook: PreSync
# Then the app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
annotations:
argocd.argoproj.io/sync-wave: "2"Hooks add lifecycle:
PreSync— runs before the main sync. Wait for completion before continuing.Sync— runs as part of the regular sync.PostSync— runs after the main sync, after all resources are healthy.SyncFail— runs only when the sync fails (cleanup hook).
Combine hook and sync-wave to express dependencies:
metadata:
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/sync-wave: "0"
argocd.argoproj.io/hook-delete-policy: HookSucceeded # Clean up the Job after successhook-delete-policy controls when ArgoCD removes the hook resource (HookSucceeded, HookFailed, BeforeHookCreation).
Fix 5: Helm Values Across Environments
For Helm charts, use valueFiles and values in the Application:
spec:
source:
repoURL: https://github.com/myorg/charts
path: charts/my-app
targetRevision: main
helm:
releaseName: my-app
valueFiles:
- values.yaml
- values-prod.yaml
values: |
image:
tag: v1.2.3valueFiles are merged in order; later files override earlier. The inline values block is highest priority.
For multi-environment GitOps with a single source of truth, use ApplicationSets:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app
spec:
generators:
- list:
elements:
- env: dev
url: https://dev.k8s.example.com
- env: prod
url: https://prod.k8s.example.com
template:
metadata:
name: "my-app-{{env}}"
spec:
project: default
source:
repoURL: https://github.com/myorg/manifests
path: apps/my-app
targetRevision: main
helm:
valueFiles:
- values.yaml
- "values-{{env}}.yaml"
destination:
server: "{{url}}"
namespace: my-app
syncPolicy:
automated: { prune: true, selfHeal: true }One ApplicationSet generates one Application per environment, using template substitution.
Pro Tip: For dynamic discovery, use the git or cluster generators instead of list. cluster auto-creates Applications for every registered cluster matching a label.
Fix 6: Kustomize Overlays
For Kustomize-based projects:
spec:
source:
repoURL: https://github.com/myorg/manifests
path: overlays/prod
targetRevision: main
kustomize:
images:
- my-app=registry.example.com/my-app:v1.2.3
namePrefix: prod-
commonLabels:
env: prodkustomize.images rewrites image tags without touching Git — useful for image-based deploy pipelines that update the tag via argocd app set:
argocd app set my-app --kustomize-image my-app=registry.example.com/my-app:v1.2.4The change is persisted on the Application spec (not in Git), and the new tag is reflected at the next sync.
Note: Using argocd app set for image updates is the “imperative-on-top-of-declarative” pattern. Some teams prefer this for fast deploys; purists insist on PR-driven tag bumps. Both work.
Fix 7: RBAC Across Project and Global
ArgoCD has two RBAC layers:
Global RBAC (in argocd-rbac-cm ConfigMap):
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:readonly
policy.csv: |
p, role:devs, applications, sync, default/*, allow
p, role:devs, applications, get, default/*, allow
g, github-team:engineering, role:devsProject RBAC (in AppProject):
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments
namespace: argocd
spec:
sourceRepos: ["https://github.com/myorg/payments-manifests"]
destinations:
- namespace: payments
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceBlacklist:
- group: ""
kind: ResourceQuota
roles:
- name: developer
policies:
- p, proj:payments:developer, applications, sync, payments/*, allow
groups:
- github-team:paymentsA user needs permissions at both layers. The Project layer scopes what their global role can touch (only Apps in payments, only sources from a specific repo).
Common Mistake: Defining a permissive global RBAC but a narrow Project. The Project wins — users get the intersection of permissions.
To debug, use argocd account can-i:
argocd account can-i sync applications payments/my-app
# yes / noFix 8: Repository Authentication
For private Git repos:
# HTTPS with PAT:
argocd repo add https://github.com/myorg/manifests \
--username myorg \
--password $GITHUB_TOKEN
# SSH:
argocd repo add [email protected]:myorg/manifests.git \
--ssh-private-key-path ~/.ssh/id_ed25519
# GitHub App (multi-repo, scalable):
argocd repo add https://github.com/myorg \
--github-app-id 12345 \
--github-app-installation-id 67890 \
--github-app-private-key-path ./private-key.pemFor multi-repo orgs, GitHub App auth is best — one credential for all repos under the org, with rate limits scaled by installation.
Repository creds can also be declared via Kubernetes Secrets with argocd.argoproj.io/secret-type: repo-creds:
apiVersion: v1
kind: Secret
metadata:
name: github-org
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repo-creds
stringData:
url: https://github.com/myorg
githubAppID: "12345"
githubAppInstallationID: "67890"
githubAppPrivateKey: |
-----BEGIN PRIVATE KEY-----
...Apply this once, and any Application referencing a repo under myorg uses this credential.
Still Not Working?
A few less-obvious failures:
ComparisonError: rpc error: code = Unknown desc = Manifest generation error. Helm or Kustomize failed. Checkargocd app getoutput for the underlying error — usually a missing values file orkustomization.yamlpath.OutOfSyncfor a resource with no diff visible. A controller is mutating a field ArgoCD doesn’t ignore. AddignoreDifferencesfor that field, or annotate the resource withargocd.argoproj.io/compare-options: IgnoreExtraneous.- Sync succeeds but pods crash. Health check, not sync. Look at
argocd app get→ “Health” section for the failing resource. Custom health checks viaresource.customizations.health.GROUP_KINDin the ArgoCD ConfigMap. - App stuck Progressing → Degraded loop. Resource has a
lifecyclemismatch. Common culprits: an Ingress without DNS, a Service expecting a missing Endpoint. InvalidSpecErrorafter Application apply. The Application’s YAML is invalid (typo in repoURL, wrong project name, missing destination).kubectl describe application <name> -n argocdshows details.- Sync hangs on PreSync hook. The hook Job/Pod is stuck Pending or failing.
kubectl get pod -n my-app -l job-name=db-migrateto inspect. Application out of sync after every sync. ArgoCD applies what’s in Git, but a controller (HPA, Operator) immediately changes a tracked field. AddignoreDifferencesfor that field.- Resources stuck in deletion (
Terminating). Finalizers haven’t run.argocd app sync my-app --prune --forcemay not help — finalizers run when the owner is gone. Manually remove finalizers if you’re sure.
For related Kubernetes deployment and GitOps issues, see Kubernetes CrashLoopBackOff, Kubernetes ImagePullBackOff, Helm not working, and Terraform state lock.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Helm Not Working — Release Already Exists, Stuck Upgrade, and Values Not Applied
How to fix Helm 3 errors — release already exists, another operation is in progress, --set values not applied, nil pointer template errors, kubeVersion mismatch, hook failures, and ConfigMap changes not restarting pods.
Fix: gh CLI Not Working — Auth Scopes, Multiple Accounts, PR Create Errors, and Enterprise Hosts
How to fix GitHub CLI errors — gh auth login token scopes missing, multiple accounts switching, gh pr create permission denied, GHE host auth, gh repo clone vs git clone, and API rate limits.
Fix: Turborepo Not Working — Cache Never Hits, Pipeline Not Running, or Workspace Task Fails
How to fix Turborepo issues — turbo.json pipeline configuration, cache keys, remote caching setup, workspace filtering, and common monorepo task ordering mistakes.
Fix: Docker Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error
How to fix Docker multi-platform build issues — buildx setup, QEMU registration, --platform flag usage, architecture-specific dependencies, and pushing multi-arch manifests to a registry.