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 +
AcrPullrole. .NET 8+dotnet publish /t:PublishContainerintegrates 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:latestmoves. - 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.
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. |
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.
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
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
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
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.
- ⚠️
latesttag 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.