← All posts

ACR sync and image promotion — for when Git is someone else’s religion

April 2026 · DevOps

Not every team falls in love with the same branching strategy, and not every pipeline starts from a tidy monorepo. Sometimes the artefact you trust is already a container digest in a registry, and the job is to move that digest to where production pulls from — with signatures, retention, and RBAC that make security nod instead of wince.

This post is about Azure Container Registry as the handshake between “we built something” and “the cluster may pull it,” especially when your developers would rather push an image than resolve a three-way merge. The punchline is not anti-Git — manifests in git should still point at immutable digests. The punchline is that registry-level sync is a first-class integration pattern, not a hack you apologize for in architecture reviews.

The cast of characters

IdeaRole
Build registryFast iteration, noisy tags, CI writers
Prod (or shared) registrySlower promotions, stricter RBAC, scanners
DigestThe real ID; tags are human-friendly stickers
Import / taskHow you copy between registries without a laptop in the loop

Think of promotion as teleporting a layer manifest, not rebuilding “the same Dockerfile somewhere else.” Same bytes, fewer arguments.

One-shot import: the hammer

az acr import is the CLI-shaped version of “copy this manifest into my registry.” It is ideal for pipelines that already know the source reference.

# Promote a build from the CI registry into the shared prod registry
az acr import \
  --name prodregistry \
  --source acrbuild.azurecr.io/platform/api:2026.04.15-abc123 \
  --image platform/api:2026.04.15-abc123 \
  --registry acrbuild.azurecr.io

If the source is another subscription, you lean on token/password or managed identity with az acr login against both ends in the job — never embed long-lived passwords in YAML named secrets-final-FINAL.yml.

Tasks: when you want cron, not courage

ACR tasks are useful when sync is periodic — base image patching, mirror upstream library/python, or “every night, refresh our approved golden images.” A trimmed task definition:

# acr-task-mirror.yaml — illustrative
version: v1.1.0
steps:
  - build: >-
      -t {{.Run.Registry}}/cache/python:3.12-slim
      https://github.com/your-org/dockerfiles.git#main:python-slim
    timeout: 1800
  - push:
    - "{{.Run.Registry}}/cache/python:3.12-slim"

Pair that with a trigger (timer or git) so nobody has to remember to “sync the base images” during an incident.

Promotion pipeline sketch (Azure DevOps flavour)

In CI, the artefact is already built. The promotion stage only needs identity, source ref, and target repository name:

# azure-pipelines-promote.yml — skeleton
stages:
  - stage: PromoteImage
    jobs:
      - job: import_digest
        pool: { vmImage: ubuntu-latest }
        steps:
          - task: AzureCLI@2
            inputs:
              azureSubscription: $(SERVICE_CONNECTION)
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                set -euo pipefail
                SRC="${BUILD_REGISTRY}.azurecr.io/$(IMAGE_REPO):$(Build.BuildId)"
                az acr import \
                  --name $(PROD_REGISTRY) \
                  --source "$SRC" \
                  --image $(IMAGE_REPO):$(Build.BuildId)

Helm or Kustomize in git still updates image: prodregistry.azurecr.io/app@sha256:… — Git remains the intent; the registry is the proof the bytes arrived.

Retention: where money and drama meet

Untagged manifests and forgotten nightly builds are how registries turn into expensive attic storage. A policy-shaped approach beats manual spring cleaning (Premium SKU; applies to untagged manifests — read the docs before you surprise someone who pulls by digest):

az acr config retention update \
  --registry myregistry \
  --status enabled \
  --days 30 \
  --type UntaggedManifests

Combine that with soft delete where your compliance people allow it, so “oops” is recoverable without pretending disk is infinite.

The cultural footnote

Someone will always prefer docker push to git rebase. Give them a rail: import rules, locked-down service principals, and manifests that reference digests. You are not lowering the bar — you are meeting delivery where it lives and still enforcing immutability at the cluster edge.

If you only remember one line from this post, remember this: sync the digest, pin the manifest, automate the boring parts. Everything else is commentary.