An app scales out. A new server starts, pulls inference-api:latest like all the others, and runs different code. Nobody deployed anything: someone pushed a new image under the same tag.
This explainer shows how Azure Container Registry (ACR) stores images, why a tag is not a version, how ACR Tasks builds and patches images in the cloud, and a tagging and cleanup scheme that keeps production predictable.
Key points
- A tag is a label that can move. A digest is a fingerprint that never changes. Pushing an existing tag moves it to the new image; the old image stays in the registry, untagged.
- ACR Tasks builds in Azure.
az acr builduploads the source, builds the image and pushes it, without Docker on your machine. Tasks can also rebuild on a commit, on a schedule, or when the base image is patched. - Stable tags for base images, unique tags for deployments.
lateststays out of production, and the deployed image gets a lock. acr purgedeletes the tags your filter selects, not only untagged images. For orphans only, use--untagged-only, and run every new purge with--dry-runfirst.
How images are stored
| Level | What it holds | Example |
|---|---|---|
| Registry | everything, behind one address: the login server | myacr.azurecr.io |
| Repository | all versions of one image; slashes group repositories by team or environment | production/inference-api |
| Artifact | a container image, a Helm chart or another OCI artifact | inference-api:v1.2.0 |
Permissions can be scoped to a repository or a namespace prefix, so a team only reaches its own images.
An image is a stack of layers. Every Dockerfile instruction that changes files adds one. Layers are stored by their content: when ten images share the same Python base layer, the registry keeps it once, and a server that already has it doesn't download it again. On top sits the manifest, which lists every layer and is identified by a digest, a SHA-256 hash.
The three tiers (Basic, Standard, Premium) push and pull the same way. Premium adds features for larger setups, such as geo-replication to other regions and private endpoints.
Tags and digests
A tag such as v1.2.0 or latest is a readable name. One image can carry several tags, for example 1.2.0 and stable. But a tag can move: push a new image with an existing tag, and the tag jumps to the new image. That is what happened to the new server above. And if you leave the tag out completely, Docker uses latest.
A digest is calculated from the content, so it can never point to another image:
# By tag: whatever the tag points to right nowdocker pull myacr.azurecr.io/inference-api:v1.2.0# By digest: always exactly this imagedocker pull myacr.azurecr.io/inference-api@sha256:<digest>
Tags are for people. Digests are for guarantees.
Builds in the cloud with ACR Tasks
Building on your own machine and pushing works, until five developers build the same image on five laptops. A quick task moves the build into Azure:
az acr build --registry myacr \--image inference-api:v1.0.0 .
The last argument is the build context: a local folder, a Git repository or a remote tarball. The command uploads it, runs the Docker build in Azure and pushes the result, like docker build plus docker push. Every build runs in the same environment, and ACR Tasks builds images for Linux, Windows and ARM.
A quick smoke test runs a container from the registry in the cloud and shows its output:
az acr run --registry myacr \--cmd '$Registry/inference-api:v1.0.0 python --version' \/dev/null
/dev/null means "no source context". $Registry stands for your registry; without it, the image name would be looked up on Docker Hub.
Builds that start themselves
A task created with az acr task create can start on its own:
| Trigger | Starts a build when | Good to know |
|---|---|---|
| Source code | a commit lands in a GitHub or Azure DevOps repository | pull request builds are possible, off by default |
| Timer | a cron expression matches | the schedule is in UTC |
| Base image | the base image of your app is updated | on by default; the task learns the base image on its first run |
az acr task create --registry myacr --name build-api \--image 'inference-api:{{.Run.ID}}' \--context https://github.com/org/api.git#main \--file Dockerfile --git-access-token $PAT \--schedule "0 0 * * *"
The base image trigger saves the most work. When python:3.12-slim gets a security patch, every image built on it is out of date; ACR Tasks rebuilds them automatically. It works with base images in the same registry, in another Azure container registry, or in public repositories on Docker Hub and the Microsoft Container Registry. A base image in an Azure registry triggers the rebuild right away; public base images are checked every 10 to 60 minutes. Only the base image of the final stage is tracked.
One condition: the base image must keep a stable tag, like 3.12-slim. If the patch arrives under a new tag, nothing is triggered.
For more than one step, a task is written in YAML. This one builds the image, runs the tests inside it, and pushes only at the end:
version: v1.1.0steps:- build: -t $Registry/inference-api:$ID .- cmd: $Registry/inference-api:$ID python -m pytest- push:- $Registry/inference-api:$ID
The steps run in order, and a failed step fails the whole run, so an image with failing tests is never pushed. $ID is the run ID: every build gets its own unique tag.
Tags for production
| Stable tags | Unique tags | |
|---|---|---|
| Reused? | yes, they move to every new version | never |
| Examples | 1 (newest 1.x), 1.1 (newest patch of 1.1) | build ID, timestamp, commit hash |
| Right for | base images: patches flow in | deployments: every server pulls the same image, a rollback is the previous tag |
The build ID is usually the best unique tag, because it leads straight back to the pipeline run with its logs and test results. A commit hash can repeat: a base image rebuild uses the same commit.
The rule: stable tags for base images, unique tags for deployments, and latest stays out of production.
Then protect what is running. A locked image can't be overwritten or deleted by accident until it is unlocked:
az acr repository update --name myacr \--image inference-api:v1.2.0 --write-enabled false
Cleaning up without deleting production
Every moved tag leaves an untagged image behind, and over time these orphans fill the registry and the bill. The cleanup tool is acr purge (in preview), which runs as a task inside the registry, on demand or on a schedule.
The common mistake: the filter selects tags. Purge deletes every matching tag older than --ago, and --untagged removes untagged images in addition. So --filter 'inference-api:.*' --untagged --ago 30d deletes every tag in that repository older than 30 days, including the one in production, unless it is locked.
For the orphans only, and as a preview first:
az acr run --registry myacr \--cmd "acr purge --untagged-only --ago 7d --dry-run" \/dev/null
On the Premium tier, a retention policy deletes untagged images automatically after a number of days (still in preview). One warning for both: a system that pulls by digest may need an image that has no tag, and cleanup deletes untagged images. Unique tags avoid that problem.

