πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Deployments intermediate ⏱ 15 minutes K8s 1.25+

ArgoCD PreSync and PostSync Hooks

Use ArgoCD PreSync hooks for database migrations and PostSync hooks for smoke tests, with SyncFail hooks for automated rollback and cleanup.

By Luca Berton β€’ β€’ Updated February 26, 2026 β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: Annotate Jobs with argocd.argoproj.io/hook: PreSync to run database migrations before deployment, PostSync for smoke tests after deployment, and SyncFail for cleanup when a sync fails.

The Problem

Application deployments often need pre-flight and post-flight actions:

  • Before deployment β€” run database migrations, validate configs, check prerequisites
  • After deployment β€” run smoke tests, send notifications, warm caches
  • On failure β€” clean up partial deployments, send alerts, trigger rollback

Kubernetes has no native concept of deployment hooks with these semantics. ArgoCD fills the gap.

The Solution

Step 1: PreSync Hook β€” Database Migration

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  namespace: myapp
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "-1"
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: myapp/api:v1.2.0
          command: ["python", "manage.py", "migrate", "--noinput"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: myapp-db-credentials
                  key: url

Step 2: PostSync Hook β€” Smoke Tests

apiVersion: batch/v1
kind: Job
metadata:
  name: smoke-test
  namespace: myapp
  annotations:
    argocd.argoproj.io/hook: PostSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: smoke
          image: curlimages/curl:8.10.0
          command:
            - /bin/sh
            - -c
            - |
              echo "Running smoke tests..."
              # Test health endpoint
              HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://myapp-api.myapp.svc:8080/health)
              if [ "$HTTP_CODE" != "200" ]; then
                echo "FAIL: health endpoint returned $HTTP_CODE"
                exit 1
              fi
              echo "PASS: health endpoint OK"

              # Test API endpoint
              HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://myapp-api.myapp.svc:8080/api/v1/status)
              if [ "$HTTP_CODE" != "200" ]; then
                echo "FAIL: API status returned $HTTP_CODE"
                exit 1
              fi
              echo "PASS: API status OK"
              echo "All smoke tests passed!"

Step 3: SyncFail Hook β€” Failure Notification

apiVersion: batch/v1
kind: Job
metadata:
  name: sync-failure-notify
  namespace: myapp
  annotations:
    argocd.argoproj.io/hook: SyncFail
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: notify
          image: curlimages/curl:8.10.0
          command:
            - /bin/sh
            - -c
            - |
              curl -X POST "$SLACK_WEBHOOK" \
                -H 'Content-Type: application/json' \
                -d '{
                  "text": "🚨 ArgoCD sync FAILED for myapp! Check the ArgoCD dashboard.",
                  "channel": "#deployments"
                }'
          env:
            - name: SLACK_WEBHOOK
              valueFrom:
                secretKeyRef:
                  name: slack-webhook
                  key: url

Sync Lifecycle

flowchart TD
    A[Sync Triggered] --> B[PreSync Phase]
    B --> B1["Job: db-migrate"]
    B1 -->|Success| C[Sync Phase]
    B1 -->|Failure| F[SyncFail Phase]
    C --> C1[Apply Manifests]
    C1 -->|All Healthy| D[PostSync Phase]
    C1 -->|Failure| F
    D --> D1["Job: smoke-test"]
    D1 -->|Success| E["βœ… Sync Complete"]
    D1 -->|Failure| F
    F --> F1["Job: sync-failure-notify"]
    F1 --> G["❌ Sync Failed"]

Hook Delete Policies

PolicyBehavior
BeforeHookCreationDelete previous hook before creating new one (default)
HookSucceededDelete hook after it succeeds
HookFailedDelete hook after it fails
# Keep failed hooks for debugging, clean up successful ones
annotations:
  argocd.argoproj.io/hook-delete-policy: HookSucceeded

Step 4: Combining Hooks with Sync Waves

# Wave -2: PreSync config validation
apiVersion: batch/v1
kind: Job
metadata:
  name: validate-config
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/sync-wave: "-2"
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: validate
          image: myapp/config-validator:latest
          command: ["validate", "--strict"]
---
# Wave -1: PreSync database migration
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/sync-wave: "-1"
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: myapp/api:v1.2.0
          command: ["python", "manage.py", "migrate"]

Common Issues

Hook Job Never Completes

Hooks must reach a terminal state (Complete or Failed). Ensure:

  • restartPolicy: Never (not Always)
  • backoffLimit is set to prevent infinite retries
  • Container command actually exits

Hook Runs on Every Sync

This is expected. Use BeforeHookCreation to clean up old hook resources.

PreSync Hook Blocks Deployment

If a PreSync hook fails, the sync stops. Use backoffLimit and check Job logs:

argocd app get myapp --show-operation
kubectl logs job/db-migrate -n myapp

Best Practices

  • Use PreSync for migrations β€” never deploy code that needs a schema change without migrating first
  • Use PostSync for validation β€” smoke tests catch issues before users hit them
  • Use SyncFail for alerts β€” automated notification prevents silent failures
  • Set backoffLimit β€” prevent infinite retry loops
  • Use BeforeHookCreation as default delete policy β€” prevents old Job conflicts
  • Keep hooks idempotent β€” they may run multiple times

Key Takeaways

  • ArgoCD hooks run at specific sync lifecycle phases: PreSync, Sync, PostSync, SyncFail
  • Hooks are typically Jobs but can be any Kubernetes resource
  • Combine hooks with sync waves for fine-grained ordering within each phase
  • Delete policies control hook cleanup β€” BeforeHookCreation is the safest default
#argocd #gitops #hooks #presync #postsync #migrations
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens