Skip to content

Azure Container Registry

Key Points

  • ACR is Azure's managed Docker / OCI registry — private images for AKS, Container Apps, App Service, Container Instances.
  • Three SKUs: Basic, Standard, Premium. You almost always want Premium — geo-replication, private endpoints, scoped tokens, content trust, retention.
  • Authentication: Entra ID + managed identity is the modern path. Avoid the admin user (deprecated). Scoped tokens for fine-grained pull-only.
  • ACR Tasks build images server-side: source-trigger, base-image-trigger, multi-step pipelines — no self-hosted build agents.
  • Pull-from-AKS / ACA / App Service with managed identity + AcrPull role. .NET 8+ dotnet publish /t:PublishContainer integrates cleanly.

Concepts (deep dive)

Why a container registry exists

A registry is the distribution layer for container images: a network service that stores image manifests + their layers and serves them to anywhere that needs to run a container. Without a registry, an image only exists on the machine that built it; with one, "deploy" becomes "tell the orchestrator to pull this digest from that registry." Three jobs the registry uniquely handles:

  • Sharing without re-shipping — layers are content-addressed, so a pull only fetches layers the host doesn't already cache. Common bases (chiseled aspnet, alpine) live in the host's layer cache after the first pull.
  • Provenance and trust — the digest is the cryptographic identity of the image. A deployment pinned to @sha256:… is bit-identical across regions and time, regardless of how :latest moves.
  • Auth and audit — who is allowed to push, who is allowed to pull, when, and from where. This is exactly why running on Docker Hub for a production app is uncomfortable: you don't own the auth model.

What ACR is

A managed OCI-compliant registry with Azure-native auth, networking, and tooling. Stores Docker images, Helm charts, and arbitrary OCI artifacts. Same wire protocol as Docker Hub, GHCR, ECR — but with private endpoints, AAD identity, and Azure RBAC.

your code ──► docker build ──► docker push myreg.azurecr.io/api:v1 ──► ACR
AKS / ACA / App Service ◄── docker pull (managed identity) ◄────────────┘

SKU tiers

ACR's SKUs aren't a "more storage" upsell — they're a feature gate. The advanced security and networking primitives (private endpoints, scoped tokens, geo-replication, retention) are Premium-only. For production, the question is almost never "Basic or Premium" — it's "is this important enough to justify Premium." For anything that touches real data, it is.

Feature Basic Standard Premium
Storage included 10 GB 100 GB 500 GB
Throughput Low Medium Highest
Geo-replication
Private endpoint
Customer-managed keys
Content trust
Scoped tokens
Retention policies
Repository-scoped permissions
Trust policy

💡 The price gap from Standard to Premium is small versus what you give up. For anything beyond personal projects, default to Premium.

Storage layout

Registry: myreg.azurecr.io
  └── Repository: services/api
        ├── Tag: v1.2.3   ──► manifest (digest sha256:abc...)
        ├── Tag: latest   ──► same manifest
        └── Manifest sha256:def...
              ├── Layer sha256:111... (50 MB)
              ├── Layer sha256:222... (10 MB)
              └── Config sha256:333...

Layers are deduped at the registry level — pushing two images sharing 90% of layers stores them once. Tags are mutable pointers to immutable manifests; manifests are immutable bags of layer digests.

az acr repository list --name myreg
az acr repository show-tags --name myreg --repository services/api
az acr repository show --name myreg --image services/api@sha256:abc...

Authentication

ACR's auth model has converged on Entra ID + RBAC as the only sensible path. The legacy "admin user" (a single shared username/password baked into the registry) is still in the product but is the equivalent of leaving a root password in a sticky note — non-rotatable, non-auditable, and impossible to scope. Disable it on day one.

Method Use
Entra ID + managed identity Production. AKS, ACA, App Service, Functions, VMs.
Entra ID user / service principal CI pipelines (when OIDC isn't possible).
Repository-scoped tokens (Premium) Pull-only / push-only for narrow scenarios (CI agents, edge devices).
Admin user Single shared username/password. Deprecated. Disable it.
# Login as user
az acr login --name myreg

# AKS: attach ACR to cluster (grants AcrPull to kubelet identity)
az aks update -n mycluster -g rg --attach-acr myreg

# Container App: assign role to its managed identity
az role assignment create \
  --assignee <aca-mi-principal-id> \
  --role AcrPull \
  --scope $(az acr show -n myreg --query id -o tsv)
// .NET: pulling private image isn't done from app code, but pushing in CI:
// Use 'az acr login' or docker login with AAD token, not stored creds.

Scoped tokens (Premium)

Restrict to specific repositories and actions.

# Token that can only pull services/api
az acr scope-map create -n pull-api --registry myreg \
  --repository services/api content/read metadata/read
az acr token create -n token-api --registry myreg --scope-map pull-api

Use case: edge device that pulls one image; can't see other repos.

ACR Tasks

ACR Tasks are server-side image builds — you hand ACR a build context (local folder or Git URL), and ACR runs the build inside its own infrastructure. The killer feature is the base-image trigger: when Microsoft publishes a CVE patch for aspnet:9.0, every image FROM-line pointing at that tag rebuilds automatically. No CI to maintain, no agents to provision, no "we'll patch next sprint."

Build images on Azure without your own Docker engine.

# One-shot build from local context
az acr build -t services/api:v1 -r myreg .

# Source-trigger: rebuild on Git commit
az acr task create \
  --name build-api \
  --registry myreg \
  --image services/api:{{.Run.ID}} \
  --context https://github.com/org/api.git \
  --branch main \
  --file Dockerfile

# Base-image trigger: rebuild when mcr.microsoft.com/dotnet/aspnet:8.0 updates
# (Created automatically when Dockerfile FROM-line uses tracked base.)

# Multi-step task (acr-task.yaml)
version: v1.1.0
steps:
  - id: build
    build: -t myreg.azurecr.io/api:{{.Run.ID}} .
  - id: test
    cmd: myreg.azurecr.io/api:{{.Run.ID}} dotnet test
  - id: push
    push: ["myreg.azurecr.io/api:{{.Run.ID}}"]

Why: keeps build secrets in Azure, no self-hosted agents, automatic rebuilds when base images get CVE patches.

Geo-replication (Premium)

Geo-replication keeps one logical registry with multiple regional copies behind a single name. Pushes land in the home region and replicate asynchronously to the rest; pulls go to the geographically closest replica, avoiding cross-region egress charges and the latency spikes of pulling a 500 MB image across an ocean. Each replica also has its own throughput budget, so a global app doesn't share rate limits.

Replicate the registry to multiple regions; clients pull from the nearest.

az acr replication create -r myreg -l westeurope
az acr replication list -r myreg -o table

Eventual consistency — a push to East US may take seconds to minutes to surface in West Europe. Don't deploy to all regions instantly after a push; wait or check the replica.

Content trust (signing)

Tool Status
Notary v1 (Docker Content Trust) Legacy; still in ACR Premium.
Notation / Notary v2 Current direction; uses Azure Key Vault for keys.
Cosign / sigstore Industry-standard alternative; ACR supports OCI artifact signatures.
# Notation example
notation sign --signature-format cose myreg.azurecr.io/api:v1
notation verify myreg.azurecr.io/api:v1

Pair with AKS image policy / Ratify to block unsigned images at deploy time.

Vulnerability scanning

Option Notes
Defender for Containers Paid; scans on push + continuously; integrates with Defender for Cloud.
Trivy Free, OSS; runs in CI as a step.
Grype / Snyk Alternatives; varying licensing.
# Trivy in pipeline
trivy image --severity HIGH,CRITICAL myreg.azurecr.io/api:v1

Block PRs on HIGH/CRITICAL. Don't promote vulnerable images to prod.

Retention & cleanup

Without policy, untagged manifests accumulate forever — every CI build leaves artifacts.

# Auto-purge untagged manifests older than 30 days (Premium)
az acr config retention update -r myreg --status enabled \
  --type UntaggedManifests --days 30

# Manual purge (e.g. for old tagged versions, in a Task)
az acr run -r myreg --cmd "acr purge --filter 'services/api:.*' --ago 90d --untagged" /dev/null

Storage cost on ACR is small per GB but adds up — 10K untagged builds becomes real money.

Networking

  • Public + firewall (allowlist IPs) — ok for CI from known runners.
  • Private endpoint (Premium) — registry only reachable inside your VNet. Pair with Private DNS Zone (privatelink.azurecr.io).
  • Service Endpoint (older; ACR doesn't actually support — use private endpoint).

Hub-spoke: ACR sits in shared services / hub VNet; spokes (AKS, ACA env) reach via VNet peering + private DNS.

[ Hub VNet ]
  └── ACR (private endpoint)
       │ peered
[ Spoke: AKS ]    [ Spoke: ACA env ]

Throttling / rate limits

Public Docker Hub has aggressive anonymous rate limits; ACR has its own — generous on Premium, tighter on Basic. Exceeded → 429 Too Many Requests. Mitigate:

  • Use ACR Premium with geo-replication so each region has its own throughput budget.
  • For cross-cloud pulls, mirror Docker Hub bases into your ACR via ACR Tasks (base-image-trigger).

Cost shape

  • Per-day SKU charge (the dominant cost on Premium for small registries).
  • Storage beyond included quota (cheap per GB).
  • Network egress when pulling cross-region or out of Azure — this is the surprise. Geo-replicate to keep pulls in-region.

Connection from compute

Compute Identity Role
AKS kubelet identity AcrPull (auto via --attach-acr)
Container Apps system / user MI AcrPull
App Service system / user MI AcrPull
Functions (containerized) MI AcrPull
Container Instances MI AcrPull
# App Service: container from ACR with managed identity
az webapp config container set --name myapp --resource-group rg \
  --container-image-name myreg.azurecr.io/services/api:v1 \
  --container-registry-url https://myreg.azurecr.io
az webapp identity assign --name myapp --resource-group rg
az webapp config set --name myapp --resource-group rg \
  --generic-configurations '{"acrUseManagedIdentityCreds": true}'

.NET specifics

.NET 8+ ships with built-in container publishing. No Dockerfile required for simple cases.

dotnet publish --os linux --arch x64 \
  /t:PublishContainer \
  -p:ContainerRegistry=myreg.azurecr.io \
  -p:ContainerRepository=services/api \
  -p:ContainerImageTag=v1.2.3
<!-- csproj -->
<PropertyGroup>
  <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:8.0-alpine</ContainerBaseImage>
  <ContainerRegistry>myreg.azurecr.io</ContainerRegistry>
  <ContainerRepository>services/api</ContainerRepository>
</PropertyGroup>

For full control (multi-stage, custom users, healthchecks), keep a Dockerfile. SDK publishing is great for quick services.


Code: correct vs wrong

❌ Wrong: admin user

az acr update -n myreg --admin-enabled true
docker login myreg.azurecr.io -u myreg -p <password>

Single shared password; can't rotate; can't audit who pulled.

✅ Correct: managed identity

# Compute pulls via its MI; no creds anywhere
az role assignment create --assignee $miPrincipalId \
  --role AcrPull --scope $(az acr show -n myreg --query id -o tsv)

❌ Wrong: latest tag in production

image: myreg.azurecr.io/services/api:latest

Mutable; can't roll back deterministically.

✅ Correct: immutable digest or semver

image: myreg.azurecr.io/services/api@sha256:abc123...
# or
image: myreg.azurecr.io/services/api:1.4.7

❌ Wrong: no retention policy

Storage grows unbounded; CI churns out untagged manifests forever.

✅ Correct: enable retention + scheduled purge

az acr config retention update -r myreg --status enabled \
  --type UntaggedManifests --days 14

Design patterns for this topic

Pattern 1 — "Premium SKU + private endpoint"

  • Intent: registry only reachable from corporate network/VNets.

Pattern 2 — "Managed identity, no admin user"

  • Intent: zero-secret pulls.

Pattern 3 — "ACR Tasks for base-image rebuilds"

  • Intent: auto-patch when base images get CVEs.

Pattern 4 — "Geo-replication for global apps"

  • Intent: in-region pulls; lower egress cost; resilience.

Pattern 5 — "Sign + verify (Notation + Ratify)"

  • Intent: only signed images run in prod.

Pros & cons / trade-offs

Aspect Pros Cons
ACR Native Azure auth, networking Not multi-cloud
Premium SKU All advanced features Per-day cost
ACR Tasks No self-hosted agents Less flexible than full CI
Geo-replication Low-latency global pulls Eventual consistency
Defender scanning Continuous; integrated Paid add-on

When to use / when to avoid

  • Use ACR for any Azure-resident container workload.
  • Use Premium for production (geo, private endpoint, retention).
  • Use ACR Tasks when self-hosted build agents are a chore.
  • Avoid Basic for production.
  • Avoid the admin user — disable it.

Interview Q&A

Q1. SKU differences? Basic / Standard differ in storage + throughput. Premium adds geo-replication, private endpoints, scoped tokens, content trust, retention.

Q2. How should compute authenticate? Managed identity + AcrPull role. Avoid admin user, avoid SP secrets.

Q3. What is ACR Tasks? Server-side image builds: source-trigger, base-image-trigger, multi-step YAML. No build agent needed.

Q4. Geo-replication consistency? Eventual. Push lands in primary; replicas catch up in seconds-to-minutes.

Q5. Layer dedup? Layers are content-addressed by digest; identical layers stored once across all repos.

Q6. How to keep storage from growing? Retention policies (Premium) auto-purge untagged manifests. Schedule acr purge for old tagged versions.

Q7. Private endpoint setup? Premium SKU + create PE in your VNet + private DNS zone privatelink.azurecr.io. Disable public access.

Q8. Image signing options? Notary v1 (legacy), Notation / Notary v2 (current MS direction, AKV-backed), Cosign / sigstore.

Q9. Vulnerability scanning? Defender for Containers (paid, continuous) or Trivy in CI (free).

Q10. Why Premium even when I don't need geo? Private endpoints, scoped tokens, retention — Premium-only and usually needed.

Q11. How does .NET 8 PublishContainer integrate? Builds an OCI image directly via SDK (no Dockerfile); push to ACR via ContainerRegistry MSBuild property.

Q12. Pulling Docker Hub images through ACR? Mirror via ACR Tasks (base-image-update trigger). Avoids Docker Hub rate limits.


Gotchas / common mistakes

  • ⚠️ Admin user enabled — disable it.
  • ⚠️ latest tag in prod — non-deterministic rollback.
  • ⚠️ No retention policy — runaway storage.
  • ⚠️ Cross-region pulls — surprise egress bill.
  • ⚠️ Public ACR + no firewall — exposed to scanners (still requires auth, but unnecessary attack surface).
  • ⚠️ Forgetting to update DNS when adding private endpoint — pulls fail silently.
  • ⚠️ Pinning to mutable tag in deployment manifests — image drifts under you.

Further reading