A few years ago, a retail client called me after a failed release at 11 p.m. Their build pipeline pulled container images from a public Docker Hub account and hit a rate limit. Worse, the fallback was a shared registry password stored in a pipeline variable that half the company could read. Nobody knew which images were approved, who pushed them, or whether the “latest” tag matched what was running in production.
An Azure Container Registry fixes this by giving your team one private, managed place to store, scan, and distribute container images. It sits between your build pipeline and your runtime, whether that is Azure Kubernetes Service, App Service, or Container Apps. If it is configured badly, it becomes a shared-password liability. Configured well, it gives you identity-based access, private networking, and a clear audit trail.
This guide shows how to create a registry, push your first image, lock down access with Microsoft Entra ID and RBAC, and automate it with a pipeline. You will also learn how to monitor it, control cost, and fix the errors that waste the most time.
What Azure Container Registry Does
Azure Container Registry (ACR) is a managed, private registry for container images and related artifacts, such as Helm charts. A container image is a packaged application with its dependencies. A registry stores versions of those images so your runtime can pull them on demand.
Why not use a public registry? Public registries are fine for base images, but they give you limited control. You cannot tie access to your Entra identities, keep traffic on a private network, or keep images near your deployments. ACR gives you all three. It also supports ACR Tasks, which can build images in Azure so you do not need a Docker daemon on the build server.
Choose the Right Service Tier
ACR comes in three tiers: Basic, Standard, and Premium. They share the same core APIs and Entra authentication. They differ in storage, throughput, and advanced features.
| Capability | Basic | Standard | Premium |
|---|---|---|---|
| Included storage | 10 GiB | 100 GiB | 500 GiB |
| Throughput and concurrency | Lowest | Higher | Highest |
| Private endpoints | No | No | Yes |
| Public IP network rules | No | No | Yes |
| Geo-replication | No | No | Yes |
| Best fit | Learning, small dev teams | Typical single-region production | Private networking, multi-region, high volume |
The decision usually comes down to networking. If your security team requires that the registry has no public endpoint, you need Premium, because private endpoints are a Premium feature. If you only need a straightforward production registry, Standard is often enough. Premium also costs more, including a per-region charge for geo-replication, so do not buy it “just in case.”
Pick a region close to the services that pull images, because pulling across regions adds latency and can add data transfer cost. ACR stores data in the region where you create it, so a regional outage can make the registry unavailable unless you use geo-replication. If you are still deciding where images will run, our comparison of Web App for Containers vs AKS vs Container Instances will help you choose the runtime first.
Pro Tip: In my experience, teams over-buy Premium on day one. I start with Standard in dev and upgrade to Premium only when a real requirement, like a private endpoint, shows up in the design review.
How To Create Azure Container Registry
Start with the boundaries. A resource group is a logical container for related Azure resources, and a subscription is the billing and governance boundary above it. I keep the registry in a shared “platform” resource group, separate from application resource groups, because many apps pull from it.
If you need a refresher, see how to create a resource group in Azure, and use a consistent resource group naming convention.
Sign in first. If you are unsure about sign-in methods, our Azure CLI login guide covers them.
az group create \
--name rg-platform-acr-prod \
--location eastus \
--tags environment=prod owner=platform-team costcenter=<cost-center>This creates the resource group rg-platform-acr-prod in East US. The --tags parameter attaches metadata, so cost reviews can group spending by owner and cost center. Read more about Azure tags before you set a company standard.
az acr create \
--resource-group rg-platform-acr-prod \
--name <registry-name> \
--sku Standard \
--location eastus \
--admin-enabled falseThis creates the registry. The --name must be globally unique, use only lowercase letters and numbers, and be 5 to 50 characters long, because it becomes <registry-name>.azurecr.io. The --sku Standard parameter selects the tier. The --admin-enabled false parameter keeps the shared admin user off.
That admin account gives registry-wide push and pull access to every caller, and you cannot tell users apart in the logs. It is disabled by default on new registries, but I set it explicitly so the intent is visible in scripts and reviews.
Pro Tip: I include the environment in the registry name, like
acrcontosoprod, but I also tag it. Names cannot change later, and tags are what finance actually filters on.
Build and Push Your First Image
Now put something in the registry. Here is a minimal Dockerfile for an internal web API.
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY ./publish .
USER app
ENTRYPOINT ["dotnet", "EmployeePortal.dll"]This starts from Microsoft’s ASP.NET runtime image, copies the published app, and runs it as a non-root user. Running as non-root limits damage if the container is compromised. Keep base images current, because old base layers carry known vulnerabilities.
Option One: Build in Azure With ACR Tasks
az acr build \
--registry <registry-name> \
--image employee-portal:v1 \
.The az acr build command uploads your source folder (the . at the end) to Azure, builds the image there, and pushes it to the registry. The --image parameter sets the repository name and tag. This approach avoids installing Docker on build agents and keeps build compute inside Azure.
Option Two: Build Locally and Push
az acr login --name <registry-name>
docker tag employee-portal:v1 <registry-name>.azurecr.io/employee-portal:v1
docker push <registry-name>.azurecr.io/employee-portal:v1The az acr login command exchanges your Entra sign-in for a short-lived registry token and configures Docker with it, so you never type a password. The docker tag command adds the registry’s address to the image name, and docker push uploads it. Confirm the result with az acr repository list --name <registry-name> --output table.
Use version tags such as v1 or the build ID instead of only latest. A moving latest tag makes rollbacks and audits hard.
Pro Tip: I tag every production image with the pipeline build ID and the Git commit hash. When someone asks what is running in prod, I can answer in one command and trace it to code.
Secure Your Azure Container Registry With Identity and RBAC
This is the part most teams get wrong. The goal is simple: no shared passwords. Microsoft Entra ID is the identity service Azure uses for authentication. Role-based access control (RBAC) grants permissions through roles.
Pair them with managed identities, which are identities Azure creates and rotates for a resource, so your code never stores a secret.
ACR has built-in roles for the common jobs:
- AcrPull: Lets an identity pull images. Give this to runtime services.
- AcrPush: Lets an identity push and pull. Give this to build pipelines.
- Reader or Contributor: Controls management of the registry resource itself, not the images.
Newer registries can also use repository-scoped permissions, so a pipeline can push to only one repository. Check whether your registry has that mode enabled, because the role names differ (“Container Registry Repository Reader” instead of AcrPull). For the basics, see our overview of Azure RBAC.
Let a Runtime Pull Images Without a Password
Assume an App Service or Container App has a system-assigned managed identity. Grant it pull access at the registry scope.
ACR_ID=$(az acr show --name <registry-name> --query id --output tsv)
az role assignment create \
--assignee-object-id <managed-identity-principal-id> \
--assignee-principal-type ServicePrincipal \
--role AcrPull \
--scope $ACR_IDThe first line stores the registry’s resource ID in a variable. The az role assignment create command then assigns AcrPull to the identity, limited to this one registry through --scope. Specifying --assignee-principal-type avoids replication delays that sometimes appear with new identities.
This is least privilege in practice: the app can pull images and nothing else. To understand what the identity is, read our guide on managed identities in Azure.
When You Need a Service Principal
Some tools run outside Azure and cannot use a managed identity. In that case, create a service principal with only AcrPush or AcrPull, and set a short secret expiry. Our tutorial on creating a service principal in Azure walks through it. Store any unavoidable secret in Azure Key Vault, a service for securely storing secrets, keys, and certificates. Review how Azure Key Vault works before you wire it into a pipeline.
Never paste registry passwords into source code, YAML files, wikis, or chat. I have found them in all four places.
Test With a Non-Admin Identity
Before calling it done, sign in as an account with only AcrPull and try to push. It should fail with a permissions error. Then try a pull, which should succeed. If the push works, your role assignment is too broad.
Pro Tip: I assign roles to Entra groups rather than people. When a developer joins or leaves, I change group membership and never touch the registry.
Lock Down Networking With a Private Endpoint
By default, a registry has a public endpoint. Authentication still protects it, but a public address is still attack surface. For internal applications, I prefer to remove public access completely. This needs the Premium tier.
A virtual network is your private network boundary in Azure. A private endpoint gives a supported Azure service a private IP address