DevOpsil
GitOps
94%
Fresh

ArgoCD Image Updater for Automated Container Deployments

Sarah ChenSarah Chen8 min read

The Annotation First

Here's an ArgoCD Application that auto-deploys when a new image is pushed. Then I'll break it down.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-service
  namespace: argocd
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=ghcr.io/your-org/api-service
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/api.semver-constraint: ">=1.0.0 <2.0.0"
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: kustomization
    argocd-image-updater.argoproj.io/git-branch: main
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/k8s-manifests
    targetRevision: main
    path: apps/api-service/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Push an image tagged v1.5.0 to your registry. Image Updater detects it, commits the update to your Git repo, ArgoCD syncs. No human in the loop.

Why Image Updater

Standard GitOps has a gap. Your CI builds and pushes a new image. Then someone — or some script — has to update the image tag in the manifests repo. That manual step is where deployments stall.

ArgoCD Image Updater closes the gap:

  • Watches container registries for new tags matching your constraints
  • Updates manifests automatically via Git commit or ArgoCD parameter override
  • Respects semantic versioning — no accidental major version bumps
  • Maintains GitOps principles — every change is tracked in Git

If it's not automated, it doesn't exist. A deployment pipeline that needs a human to bump an image tag is not a pipeline.

Installation

# Install via Helm
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

helm install argocd-image-updater argo/argocd-image-updater \
  --namespace argocd \
  --set config.registries[0].name=GitHub \
  --set config.registries[0].prefix=ghcr.io \
  --set config.registries[0].api_url=https://ghcr.io \
  --set config.registries[0].credentials=pullsecret:argocd/ghcr-creds \
  --set config.registries[0].default=true

Or apply the manifests directly:

kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj-labs/argocd-image-updater/stable/manifests/install.yaml

Registry Authentication

Image Updater needs to read tags from your registry:

apiVersion: v1
kind: Secret
metadata:
  name: ghcr-creds
  namespace: argocd
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: <base64-encoded-docker-config>
# Create the secret from a token
kubectl create secret docker-registry ghcr-creds \
  --namespace argocd \
  --docker-server=ghcr.io \
  --docker-username=your-bot-user \
  --docker-password=$GHCR_TOKEN

Use a bot account with read-only access. Never your personal token.

Update Strategies

annotations:
  argocd-image-updater.argoproj.io/image-list: app=ghcr.io/org/app
  argocd-image-updater.argoproj.io/app.update-strategy: semver
  argocd-image-updater.argoproj.io/app.semver-constraint: ">=1.0.0 <2.0.0"

Only picks up patch and minor versions within your constraint. A v2.0.0 tag is ignored. This is the safe default.

Latest Build (By Timestamp)

annotations:
  argocd-image-updater.argoproj.io/image-list: app=ghcr.io/org/app
  argocd-image-updater.argoproj.io/app.update-strategy: latest
  argocd-image-updater.argoproj.io/app.allow-tags: "regexp:^main-[a-f0-9]{7}$"

Picks the most recently built image matching the tag pattern. Good for dev/staging where you want every commit deployed.

Digest-Based

annotations:
  argocd-image-updater.argoproj.io/image-list: app=ghcr.io/org/app:stable
  argocd-image-updater.argoproj.io/app.update-strategy: digest

Watches a mutable tag and triggers updates when the digest changes. Useful when tags like stable are overwritten.

Write-Back Methods

annotations:
  argocd-image-updater.argoproj.io/write-back-method: git
  argocd-image-updater.argoproj.io/write-back-target: kustomization
  argocd-image-updater.argoproj.io/git-branch: main

Image Updater commits a .argocd-source-<app>.yaml file or updates your kustomization.yaml. True GitOps — every image change is a Git commit.

You need a Git write-back secret:

apiVersion: v1
kind: Secret
metadata:
  name: git-creds
  namespace: argocd
type: Opaque
data:
  username: <base64>
  password: <base64>

ArgoCD Parameter Override

annotations:
  argocd-image-updater.argoproj.io/write-back-method: argocd

Updates the image tag via ArgoCD's parameter override. Faster but not persisted in Git. If ArgoCD restarts, the override is lost. Use Git write-back for production.

Multi-Container Applications

annotations:
  argocd-image-updater.argoproj.io/image-list: >
    api=ghcr.io/org/api,
    worker=ghcr.io/org/worker,
    migrator=ghcr.io/org/migrator
  argocd-image-updater.argoproj.io/api.update-strategy: semver
  argocd-image-updater.argoproj.io/api.semver-constraint: ">=2.0.0 <3.0.0"
  argocd-image-updater.argoproj.io/worker.update-strategy: semver
  argocd-image-updater.argoproj.io/worker.semver-constraint: ">=1.0.0 <2.0.0"
  argocd-image-updater.argoproj.io/migrator.update-strategy: digest

Each image gets its own alias and strategy. The migrator uses digest because it's always tagged latest.

Kustomize Integration

When using Git write-back with Kustomize, Image Updater modifies your kustomization.yaml:

# kustomization.yaml (before)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml

# After Image Updater runs, it adds:
images:
  - name: ghcr.io/org/api-service
    newTag: v1.5.0

Clean. Auditable. git log shows exactly when each image version was deployed.

Monitoring

Check what Image Updater is doing:

# View logs
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-image-updater

# Check which applications are being tracked
kubectl get applications -n argocd -o json | \
  jq '.items[] | select(.metadata.annotations["argocd-image-updater.argoproj.io/image-list"]) | .metadata.name'

Image Updater also exposes Prometheus metrics on port 8081:

# ServiceMonitor for Prometheus
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: argocd-image-updater
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: argocd-image-updater
  endpoints:
    - port: metrics

Troubleshooting

Problem: Image Updater doesn't detect new tags. Fix: Check registry credentials. Run argocd-image-updater test <image> to verify access.

Problem: Git write-back commits fail. Fix: Ensure the Git credentials secret exists in the argocd namespace and the bot user has write access.

Problem: Wrong image gets selected. Fix: Tighten your allow-tags regex or semver-constraint. Use argocd-image-updater test <image> --semver-constraint ">=1.0.0" to preview matches.

Filtering Tags with Regex

Real-world registries have noisy tags. Filter them precisely.

annotations:
  # Only match tags like: v1.2.3, v1.2.3-rc.1
  argocd-image-updater.argoproj.io/app.allow-tags: "regexp:^v[0-9]+\\.[0-9]+\\.[0-9]+(-rc\\.[0-9]+)?$"

  # Ignore pre-release tags in production
  argocd-image-updater.argoproj.io/app.ignore-tags: "regexp:(-alpha|-beta|-rc)"

Without tag filtering, Image Updater might pick up a dev-abc1234 tag pushed by a feature branch build. Always constrain what it can deploy.

Per-Environment Configuration

Different environments need different update strategies. Here's a pattern that works.

Development: Deploy Every Commit

# dev/api-service.yaml
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=ghcr.io/org/api
    argocd-image-updater.argoproj.io/api.update-strategy: latest
    argocd-image-updater.argoproj.io/api.allow-tags: "regexp:^main-[a-f0-9]{7}$"
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: deploy/dev

Every commit to main deploys to dev within minutes. The deploy/dev branch keeps dev changes separate from prod manifests.

Staging: Deploy Release Candidates

# staging/api-service.yaml
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=ghcr.io/org/api
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/api.semver-constraint: ">=1.0.0-rc.0"
    argocd-image-updater.argoproj.io/api.allow-tags: "regexp:^v[0-9]+\\.[0-9]+\\.[0-9]+-rc"
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: deploy/staging

Production: Only Stable Releases

# prod/api-service.yaml
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=ghcr.io/org/api
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/api.semver-constraint: ">=1.0.0 <2.0.0"
    argocd-image-updater.argoproj.io/api.ignore-tags: "regexp:(-rc|-alpha|-beta)"
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: deploy/prod

Each environment has its own branch for write-back. This gives you a full Git history of what was deployed where and when.

Helm Values Integration

If your applications use Helm charts, Image Updater can update Helm values instead of Kustomize:

metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: api=ghcr.io/org/api
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/api.helm.image-name: image.repository
    argocd-image-updater.argoproj.io/api.helm.image-tag: image.tag
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: helmvalues:values.yaml

Image Updater writes the new tag into your values.yaml:

# values.yaml (after Image Updater update)
image:
  repository: ghcr.io/org/api
  tag: v1.5.0

This works with any Helm chart structure. Just point the helm.image-name and helm.image-tag annotations to the correct value paths.

Rollback Strategy

Automated deployments need automated rollbacks. When Image Updater deploys a bad version, you need to recover fast.

Git Revert

Since every image update is a Git commit, rollback is a git revert:

# Find the Image Updater commit
git log --oneline --author="argocd-image-updater" -5

# Revert the bad deployment
git revert abc1234
git push origin deploy/prod

ArgoCD syncs the reverted manifest and rolls back the deployment. Full audit trail in Git.

Pin to a Known Good Version

For immediate mitigation, override the image in ArgoCD directly:

argocd app set api-service \
  --helm-set image.tag=v1.4.0

This overrides Image Updater temporarily. Fix the root cause, then remove the override and let Image Updater resume.

Alerting on Update Failures

Image Updater can fail silently. Monitor it proactively.

# Prometheus alert for Image Updater failures
groups:
  - name: argocd-image-updater
    rules:
      - alert: ImageUpdaterUpdateFailed
        expr: |
          increase(argocd_image_updater_images_updated_errors_total[1h]) > 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "ArgoCD Image Updater failed to update images"
          runbook: "https://wiki.internal/runbooks/image-updater-failure"

      - alert: ImageUpdaterNotRunning
        expr: |
          absent(argocd_image_updater_images_checked_total) == 1
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "ArgoCD Image Updater is not running"

Check logs regularly to spot issues before they become incidents:

# Show recent update activity
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-image-updater \
  --since=1h | grep -E "(updated|error|failed)"

Common Pitfalls

Pitfall 1: Not testing in a non-prod environment first. Image Updater doesn't know if your new image is healthy. Use dev/staging environments with the same setup. If v1.5.0 breaks staging, it never reaches prod.

Pitfall 2: Overly broad tag matching. Without allow-tags or ignore-tags, Image Updater picks up every tag including CI builds and debug images. Always constrain the tag pattern.

Pitfall 3: Missing health checks on deployments. If the new image crashes, Kubernetes rolls back the Deployment only if you have proper readinessProbe and livenessProbe configured. Without health checks, a crashing container keeps restarting indefinitely.

Pitfall 4: Write-back branch conflicts. If multiple Image Updater instances write to the same branch simultaneously, you get merge conflicts. Use separate branches per environment or per application.

Pitfall 5: Forgetting to configure the update interval. Default check interval is 2 minutes, which means high registry API usage. For production, increase to 5-10 minutes to reduce API calls and rate limit risk.

Conclusion

ArgoCD Image Updater removes the last manual step from your GitOps pipeline. CI builds and pushes images. Image Updater detects them, updates manifests in Git, and ArgoCD syncs to the cluster. Use semver constraints for production, latest strategy for dev, and always prefer Git write-back. Filter tags aggressively, separate environments with dedicated branches, and always have a rollback plan. Fully automated, fully auditable, fully GitOps.

Share:
Sarah Chen
Sarah Chen

CI/CD Engineering Lead

Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.

Related Articles