ArgoCD Image Updater#

ArgoCD Image Updater watches container registries for new image tags and automatically updates ArgoCD Applications to use them. In a standard GitOps workflow, updating an image tag requires a Git commit that changes the tag in a values file or manifest. Image Updater automates that step.

The Problem It Solves#

Standard GitOps image update flow:

CI builds image → pushes myapp:v1.2.3 to registry
    → Developer (or CI) commits "update image tag to v1.2.3" to Git
    → ArgoCD detects Git change
    → ArgoCD syncs new tag to cluster

That middle step – committing the tag update – is friction. CI pipelines need Git write access, commit messages are noise (“bump image to v1.2.4”, “bump image to v1.2.5”), and the delay between image push and deployment depends on how fast the commit pipeline runs.

With Image Updater:

CI builds image → pushes myapp:v1.2.3 to registry
    → Image Updater detects new tag in registry
    → Updates ArgoCD Application (or writes back to Git)
    → ArgoCD syncs new tag to cluster

Installation#

helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd-image-updater argo/argocd-image-updater \
  --namespace argocd

Or with plain manifests:

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

Image Updater runs as a separate Deployment in the ArgoCD namespace. It talks to the ArgoCD API server to read and update Application resources.

Configuring an Application for Image Updates#

Image Updater is configured entirely through annotations on ArgoCD Application resources. No changes to the Application spec itself are needed.

Basic Example#

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
  annotations:
    argocd-image-updater.argoproj.io/image-list: myapp=myorg/my-app
    argocd-image-updater.argoproj.io/myapp.update-strategy: semver
spec:
  source:
    repoURL: https://github.com/myorg/gitops-config.git
    targetRevision: main
    path: apps/my-app
    helm:
      parameters:
        - name: image.tag
          value: "1.0.0"
  # ... rest of spec

The image-list annotation maps an alias (myapp) to a container image (myorg/my-app). The update strategy (semver) tells Image Updater how to pick the latest tag.

Annotation Reference#

Annotation Purpose Example
image-list Images to track (alias=image) myapp=myorg/my-app
<alias>.update-strategy How to select new tags semver, latest, digest, name
<alias>.allow-tags Regex filter for allowed tags regexp:^v[0-9]+\.[0-9]+\.[0-9]+$
<alias>.ignore-tags Regex filter for ignored tags regexp:.*-rc.*
<alias>.helm.image-name Helm parameter for image name image.repository
<alias>.helm.image-tag Helm parameter for image tag image.tag
<alias>.helm.image-spec Helm parameter for full image spec image.name (for repo:tag format)
<alias>.platforms Filter by platform/arch linux/amd64,linux/arm64

Update Strategies#

semver#

Selects the highest tag that matches semantic versioning. This is the most common strategy for production.

annotations:
  argocd-image-updater.argoproj.io/image-list: myapp=myorg/my-app
  argocd-image-updater.argoproj.io/myapp.update-strategy: semver
  argocd-image-updater.argoproj.io/myapp.allow-tags: "regexp:^[0-9]+\\.[0-9]+\\.[0-9]+$"

With tags 1.0.0, 1.1.0, 1.2.0-rc1, 1.2.0, and 2.0.0 in the registry, and the allow-tags filter excluding prerelease suffixes, Image Updater selects 2.0.0.

Constrain to a major or minor version:

annotations:
  argocd-image-updater.argoproj.io/myapp.update-strategy: semver
  argocd-image-updater.argoproj.io/myapp.allow-tags: "regexp:^1\\.2\\.[0-9]+$"

This only updates within the 1.2.x range.

latest#

Selects the most recently pushed tag, regardless of tag name. Useful for development environments tracking a main branch:

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

This picks the most recently pushed tag matching main-<short-sha>.

digest#

Tracks a mutable tag (like latest or main) by its digest. When the digest changes, Image Updater updates the Application:

annotations:
  argocd-image-updater.argoproj.io/image-list: myapp=myorg/my-app:main
  argocd-image-updater.argoproj.io/myapp.update-strategy: digest

The Application image reference changes from myorg/my-app:main to myorg/my-app@sha256:abc123..., pinning to the exact image content.

name#

Selects the tag with the highest lexicographic sort. Useful for date-based tags like 20260222 or build-1234:

annotations:
  argocd-image-updater.argoproj.io/myapp.update-strategy: name
  argocd-image-updater.argoproj.io/myapp.allow-tags: "regexp:^[0-9]{8}$"

Registry Authentication#

Image Updater needs read access to the container registry to list tags and check digests.

Docker Hub#

kubectl -n argocd create secret docker-registry dockerhub-creds \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username=myuser \
  --docker-password=mytoken
annotations:
  argocd-image-updater.argoproj.io/myapp.pull-secret: pullsecret:argocd/dockerhub-creds

Amazon ECR#

ECR tokens expire every 12 hours. Use the ECR credential helper or a CronJob that refreshes the token:

# Image Updater config in argocd-image-updater-config ConfigMap
data:
  registries.conf: |
    registries:
      - name: ECR
        prefix: 123456789012.dkr.ecr.us-east-1.amazonaws.com
        api_url: https://123456789012.dkr.ecr.us-east-1.amazonaws.com
        credentials: ext:/scripts/ecr-login.sh
        credsexpire: 10h

Or use IRSA (IAM Roles for Service Accounts) to give the Image Updater pod ECR access directly.

GitHub Container Registry (ghcr.io)#

kubectl -n argocd create secret docker-registry ghcr-creds \
  --docker-server=ghcr.io \
  --docker-username=myuser \
  --docker-password=ghp_xxxxxxxxxxxx

Write-Back Methods#

When Image Updater finds a new image, it needs to update something. There are two approaches.

ArgoCD API (Default)#

Image Updater overrides the image parameter directly on the ArgoCD Application resource using parameter overrides. No Git commit is created. The Application shows the updated image in ArgoCD but Git still has the old tag.

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

Pros: Instant, no Git noise. Cons: Git is no longer the single source of truth for the running image tag. If the Application is re-synced from Git, the tag reverts.

Git Write-Back#

Image Updater commits the updated tag to Git. ArgoCD then syncs the Git change like any other commit.

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

For Kustomize applications:

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

Image Updater needs Git write access. Configure credentials:

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

Where git-creds is a Secret containing the Git credentials.

Git write-back maintains the GitOps principle of Git as the single source of truth. The tradeoff is commit noise and a slight delay while the Git commit propagates.

Write-Back to a Different Branch#

Write image updates to a separate branch to keep the main branch clean of automated commits:

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

The Application’s targetRevision must point to the image-updates branch for the changes to take effect.

Multiple Images#

Track multiple container images in a single Application:

annotations:
  argocd-image-updater.argoproj.io/image-list: frontend=myorg/frontend, backend=myorg/backend, worker=myorg/worker
  argocd-image-updater.argoproj.io/frontend.update-strategy: semver
  argocd-image-updater.argoproj.io/frontend.helm.image-tag: frontend.image.tag
  argocd-image-updater.argoproj.io/backend.update-strategy: semver
  argocd-image-updater.argoproj.io/backend.helm.image-tag: backend.image.tag
  argocd-image-updater.argoproj.io/worker.update-strategy: latest
  argocd-image-updater.argoproj.io/worker.helm.image-tag: worker.image.tag
  argocd-image-updater.argoproj.io/worker.allow-tags: "regexp:^main-[a-f0-9]{7}$"

Each image can have its own update strategy, tag filters, and Helm parameter mapping.

Checking Image Updater Status#

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

# Check which images are being tracked
kubectl get applications -n argocd -o json | jq '.items[] | select(.metadata.annotations["argocd-image-updater.argoproj.io/image-list"] != null) | {name: .metadata.name, images: .metadata.annotations["argocd-image-updater.argoproj.io/image-list"]}'

# Force an immediate check
kubectl rollout restart deployment/argocd-image-updater -n argocd

The default check interval is 2 minutes. Configure it in the Image Updater deployment:

env:
  - name: IMAGE_UPDATER_INTERVAL
    value: "5m"

Common Mistakes#

  1. Using the argocd write-back method in production. If the Application is ever hard-refreshed or recreated from Git, the image tag reverts to whatever is in Git. Use git write-back for production to keep Git as the source of truth.
  2. Not filtering tags with allow-tags. Without filters, Image Updater considers every tag in the registry, including test builds, debug images, and broken releases. Always restrict to a known tag format.
  3. Forgetting that ECR tokens expire. Static Docker credentials work for Docker Hub and ghcr.io. ECR requires a credential refresh mechanism. Image pulls silently fail after 12 hours without rotation.
  4. Setting the check interval too low for large registries. Listing tags on a registry with thousands of images every 30 seconds generates significant registry API traffic. Start at 2-5 minutes.
  5. Not accounting for multi-arch images. If you build for both amd64 and arm64, ensure the tag exists for all required platforms before Image Updater picks it up. Use the platforms annotation to filter.