Home / Azure

AzureHow-toIntermediate

Azure Container Registry keeps growing: cleaning up old images with acr purge

Find what is filling your Azure Container Registry, delete old tags and untagged manifests safely with acr purge, protect release images and schedule the clean-up.

Deepak Bhardwaj7 min read

Applies toAzure Container Registry (Basic, Standard, Premium), Azure CLI 2.50+, ACR Tasks

The symptom

Every CI build pushes a new tag, nothing ever deletes one, and after a year the registry holds tens of thousands of images. You notice it as a storage line on the bill, a registry close to its SKU's included storage, or slow az acr repository show-tags calls. The fix is a repeatable purge job, not a one-off manual delete.

This guide uses acr purge, which runs inside ACR Tasks, so you don't need Docker or a build agent.

1. Measure what is there

Check the registry size against what the SKU includes:

az acr show-usage --name myregistry --output table

List repositories and the newest tags in the noisiest ones:

az acr repository list --name myregistry --output table

az acr repository show-tags --name myregistry --repository orders-api \
  --orderby time_desc --detail --output table | head -20

Untagged manifests are the hidden part. Each time you push orders-api:latest again, the previous image loses its tag but stays in storage:

az acr manifest list-metadata --registry myregistry --name orders-api \
  --query "[?tags==null].{digest:digest, created:createdTime}" --output table

Property names in this output can vary between CLI versions; run it once without --query to see what your build returns.

2. Know what deleting actually does

Action Command Storage freed?
Remove one tag az acr repository untag No. The manifest stays as an untagged manifest
Delete an image by tag az acr repository delete --image repo:tag Yes, once no other manifest uses the layers. Deletes every tag pointing at that manifest
Delete by digest az acr repository delete --image repo@sha256:... Same as above
Purge tags only acr purge --filter ... --ago ... Partly. Untagged manifests remain unless you add --untagged
Purge tags and untagged manifests acr purge ... --untagged Yes

Layers are shared between images, so freed storage is often smaller than the sum of the image sizes. The usage figure can take a while to drop after a large delete.

Caution

Deleted images can't be pulled again. A Kubernetes node that has to re-pull a deleted image, or a pod pinned to a deleted digest, goes into ImagePullBackOff on the next restart or scale-out. Always check what is running before you purge.

3. Find the images still in use

On AKS, list every image and digest currently running:

kubectl get pods -A -o jsonpath='{range .items[*]}{range .status.containerStatuses[*]}{.image}{"\t"}{.imageID}{"\n"}{end}{end}' \
  | sort -u | grep myregistry.azurecr.io

Also check the places that don't show up in a pod list:

  • Deployment manifests and Helm values in Git for rollback targets.
  • App Service, Container Apps and Functions that pull from the registry (az webapp config container show, az containerapp show).
  • Image tags referenced by release pipelines for rollback.

4. Protect release images with locks

acr purge skips any image whose delete-enabled or write-enabled attribute is false. Lock the images you must keep:

az acr repository update --name myregistry \
  --image orders-api:1.14.2 --delete-enabled false --write-enabled false

# Check the attributes
az acr repository show --name myregistry --image orders-api:1.14.2 \
  --query changeableAttributes

A simpler pattern is a tag naming convention: CI builds push build-<number> tags, releases push semantic version tags, and the purge filter only matches build- tags.

5. Dry-run the purge

acr purge takes a repository and a tag regex in --filter, an age in --ago (Go duration format, with d for days, such as 30d or 2d6h), and optionally --keep for the number of most recent matching tags to retain. Run it from Bash or Cloud Shell:

PURGE_CMD="acr purge \
  --filter 'orders-api:^build-.*' \
  --filter 'billing-worker:^build-.*' \
  --ago 30d --keep 10 --untagged --dry-run"

az acr run --cmd "$PURGE_CMD" --registry myregistry /dev/null

The /dev/null is the build context; the command needs none. The output lists every tag and manifest that would be deleted. Read it against the in-use list from step 3 before going further.

Tip

Start with one repository and a long --ago value (90d), check the result, then tighten. A filter of '.*:.*' matches every repository and every tag; don't use it on a shared registry until you have reviewed the dry-run.

6. Run it, then schedule it

Remove --dry-run and give large registries more time than the default task timeout (600 seconds):

PURGE_CMD="acr purge \
  --filter 'orders-api:^build-.*' \
  --filter 'billing-worker:^build-.*' \
  --ago 30d --keep 10 --untagged"

az acr run --cmd "$PURGE_CMD" --registry myregistry --timeout 3600 /dev/null

Then create a scheduled task so it keeps happening (cron in UTC):

az acr task create --name purge-build-tags \
  --cmd "$PURGE_CMD" \
  --schedule "0 2 * * *" \
  --registry myregistry \
  --timeout 3600 \
  --context /dev/null

# Trigger one run now and check the result
az acr task run --name purge-build-tags --registry myregistry
az acr task list-runs --registry myregistry --name purge-build-tags --output table

Read a run's log with az acr task logs --registry myregistry --run-id <run-id>.

7. Retention policy for untagged manifests

On Premium registries there is also a retention policy that deletes untagged manifests after a number of days. It is a preview feature; check its current status before relying on it:

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

az acr config retention show --registry myregistry

It doesn't touch tagged images, so you still need a purge task for old build tags.

Common problems

Symptom Likely cause Fix
Task run fails with an access or network error Registry denies public network access, ACR Tasks can't reach it Check the ACR Tasks network guidance for your setup (trusted services access or a dedicated agent pool on Premium)
Purge ends with a timeout Too many tags for one run Raise --timeout, split by repository, or run more often
Storage barely drops after purge Layers shared with images you kept, or untagged manifests not purged Add --untagged, then recheck usage later
An image you needed is gone Filter regex too broad, or the image had no lock Rebuild from the commit or restore from another registry; lock release tags from now on
Pods in ImagePullBackOff after purge Running workload pinned to a deleted tag or digest Redeploy with a current tag; add in-use checks to the purge process

Prevent it next time

  • Separate tag namespaces: build-* for CI, semantic versions for releases.
  • Lock every release image as part of the release pipeline.
  • Keep the purge as a scheduled ACR task, defined in the same IaC repo as the registry, not a script on someone's laptop.
  • Alert on registry storage growth. StorageUsed is a registry metric in Azure Monitor; names can vary, so check the metric list in the portal.
  • Review the dry-run output whenever you change the filters.