SaveMyCert
Log in
5 of 5 free questions left today·for 10 a day
Develop containerized solutions on Azure

AI-200 Container Hosting: Azure Container Registry, ACR Tasks and App Service Containers

21 min readAI-200 · Develop containerized solutions on AzureUpdated

Container application hosting on Azure means building and storing container images in Azure Container Registry (ACR), automating their builds with ACR Tasks, and running them on Azure App Service with configuration and secrets supplied by the platform. For the AI-200 exam you need to pick the right registry tier and authentication method, version and protect images, write task commands and YAML, and configure App Service to pull images securely and pass environment variables and Key Vault secrets to the container.

What you’ll learn
  • Choose an ACR tier from the features a scenario needs, such as geo-replication, private endpoints, retention policy and dedicated data endpoints
  • Pick the right registry authentication: Entra roles in RBAC or ABAC mode, repository-scoped tokens, or the admin user
  • Version images with unique tags and digests, import or cache external images, and lock or purge registry content
  • Build and run images with ACR quick tasks, timer, commit and base image triggers, and multi-step YAML
  • Configure App Service to pull from ACR with a managed identity, over a private network, and with continuous deployment and slots
  • Supply environment variables, connection strings and Key Vault secrets to a container on App Service

Which Azure Container Registry tier do you need?

Choose the tier by the features the scenario requires, not by storage size: Premium is the only tier with geo-replication, private endpoints, IP and virtual network access rules, the retention policy for untagged manifests and dedicated data endpoints. Basic and Standard cover the core registry operations (push, pull, ACR Tasks, tokens, image locks, artifact cache rules) with lower included storage and throughput — artifact cache works in every tier. Zone redundancy is enabled by default in supported regions for every tier.

Requirement in the scenarioFeatureTier
One login server name, a nearby copy in several regions, push onceGeo-replicationPremium
Pulls over private IP addresses, public access offPrivate endpoint (Private Link, privatelink.azurecr.io DNS)Premium
Allow only certain public IPs or subnetsIP / virtual network access rulesPremium
Registry deletes untagged manifests by itself after N daysRetention policyPremium
Firewall allow-lists only the registry's own FQDNs for layer downloadsDedicated data endpoints (<registry>.<region>.data.azurecr.io)Premium
Pull-through cache of an upstream such as Docker HubArtifact cache rulesBasic, Standard and Premium (Docker Hub needs a credential set)
Non-Entra clients scoped to some repositoriesTokens and scope mapsAll tiers

Geo-replication beats running one registry per region because every region keeps the same login server name: the pipeline pushes once, Azure replicates the content, and DNS sends each pull to the closest replica. Separate registries synced with az acr import also give nearby copies, but each has its own name. When a Standard registry must become private, the fix is two steps: upgrade the tier to Premium, then add a private endpoint in the virtual network and disable public network access.

Dedicated data endpoints matter for strict firewalls. A client authenticates and fetches manifests from the login server but downloads layers from a data endpoint, which by default is shared storage that a firewall can only allow with a wildcard such as *.blob.core.windows.net. With dedicated data endpoints turned on, the firewall allows exactly the login server plus the registry's own regional data endpoints.

How do clients authenticate to a registry?

Prefer Microsoft Entra identities with registry roles (managed identities for Azure workloads, service principals for pipelines); use repository-scoped tokens for clients that have no Entra identity; keep the admin user disabled except for quick tests, because it is a single shared registry-wide credential with push and pull rights.

Roles in the RBAC Registry Permissions mode

RoleData plane rightsUse when
AcrPullPullAn app or cluster only runs images
AcrPushPush and pullA pipeline publishes images
AcrDeleteDelete images and tagsA cleanup job removes content; combine with AcrPush for push-then-prune jobs
Contributor / OwnerData plane plus control plane (settings, role assignments)Administrators, never for least-privilege pipelines

The RBAC and ABAC mode

A registry created or switched with --role-assignment-mode rbac-abac uses attribute-based access control for repository permissions. In that mode the legacy AcrPull, AcrPush and AcrDelete roles are not honored, so an identity that only holds AcrPull gets unauthorized errors. The replacement roles are Container Registry Repository Reader, Writer and Contributor. Assigned without a condition they apply to every repository; an ABAC condition on the repository name narrows them to, for example, billing/invoices.

Tokens and scope maps

A token authenticates with its name and a generated password, and its linked scope map lists repositories and actions: content/read to pull, content/write to push (pushing needs both read and write), plus metadata and delete actions. Tokens fit edge devices, partners outside your tenant and other non-Entra clients. Issue one token per device or partner so that one can be disabled or deleted without affecting the rest. A repository wildcard is valid only with a trailing /*, as in edge-fleet/*, and it also covers repositories that a later push creates.

ARM audience tokens

Some Azure services, including App Service's managed-identity image pull, authenticate to the registry with Microsoft Entra tokens issued for the Azure Resource Manager audience. Built-in Azure Policy definitions can disable ARM-audience authentication on registries (or remediate them to disable it). When pulls fail right after a policy assignment and the role assignment is unchanged, re-enable it with az acr config authentication-as-arm update --status enabled and exempt the registry from the policy, or it is switched back.

Tags and digests: how do you version images?

Use a unique tag per build (build ID, commit SHA or date stamp) for anything you deploy, and a digest (repo@sha256:<digest>) when you need a guarantee of exact bytes; keep stable tags such as latest or 1.4 for base images and convenience, because they are expected to move.

ReferenceCan it point at different bytes later?Good for
Stable tag (latest, node:20, api:1.4)Yes, every push reassigns itBase images that should receive patches; base image update triggers
Unique tag (api:1.4.57)Only if someone pushes the same tag again; lock it with write-enabled false to prevent thatDeployments, traceable releases
Digest (api@sha256:…)Never, it is content-addressedAudited, reproducible redeployments

Deploying a stable tag causes drift: instances that start after a new push (for example during scale-out) pull the newer, possibly untested build while older instances keep the previous one. Only a push moves a tag. Replication and authentication settings never change what it points to, and a lock (write-enabled false) only stops a push from moving it.

How do you bring external images into a registry?

Use az acr import for a one-time server-side copy that keeps the original manifest digest, and an artifact cache rule when the registry should fetch an upstream image on first request and serve its stored copy afterwards.

az acr import needs only the Azure CLI: Azure pulls from the source registry (Docker Hub, MCR or another Azure registry) and writes into the target, so no local Docker engine is involved and the digest is preserved. Rebuilding an image with FROM creates a new image with a new digest instead.

az acr import --name fabrikamreg \
  --source mcr.microsoft.com/dotnet/runtime:8.0 \
  --image dotnet/runtime:8.0

For a source Azure registry in another subscription of the same tenant, pass its resource ID with --registry and the image path with --source; the import then runs with your own Entra identity, so no registry password or admin user is needed. Replication is not a copy mechanism between registries, it only replicates one registry.

Artifact cache (pull-through cache) is configured with a cache rule that maps an upstream repository, such as docker.io/library/python, to a repository in your registry. It needs the Standard or Premium tier and removes upstream rate-limit problems without copy jobs. A Docker Hub upstream supports authenticated pulls only, so its rule uses a credential set whose username and token are stored as Key Vault secrets.

How do you lock and clean up images?

Lock content with az acr repository update attributes, and clean up with acr purge (any tier, run on demand or on a schedule) or the retention policy (Premium, untagged manifests only).

Lock attributes

SettingOverwrite (push same tag)Delete
--image x:1 --write-enabled falseBlockedBlocked
--image x:1 --delete-enabled false --write-enabled trueAllowedBlocked
--repository x --delete-enabled falseImages inside can still be overwrittenThe repository can't be deleted as a whole, but images inside it can still be deleted
--repository x --write-enabled falseBlocked for every image in the repositoryBlocked for the repository and its images

So an immutable release is a single tag with write-enabled false (tag and manifest attributes are set separately, so locking a tag doesn't lock its manifest digest; deploy by the locked tag, or lock the digest too, with --image x@sha256:…); a weekly-refreshed base tag that must never be deleted uses delete-enabled false with write-enabled true, and a repository that must not disappear needs its own repository-level setting as well. To delete a write-locked image, first set --write-enabled true on that same image. An Azure resource lock (az lock) protects only management-plane operations such as deleting the registry resource; it never protects images or tags.

Purge and retention

acr purge (a preview command run inside ACR Tasks) takes a required --filter of a repository regex and a tag regex ('samples/.*:.*' matches every repository under samples/), --ago for age, --untagged to also delete manifests left without tags, --keep N to retain the newest N tags per repository that would otherwise be deleted, and --dry-run to list what would be deleted without deleting it. Add --dry-run first to see the list. az acr run executes it once; an ACR task with a timer trigger and --context /dev/null makes it recur on managed compute.

az acr run --registry reg \
  --cmd "acr purge --filter 'ci/.*:.*' --ago 30d --keep 3 --untagged" \
  /dev/null

The retention policy is a registry setting with no task to maintain: az acr config retention update --status enabled --days 7 --type UntaggedManifests deletes each untagged manifest once it has been untagged for that many days. It can't delete old tags, and it requires Premium.

What are ACR Tasks quick tasks and quick runs?

A quick task moves docker build into Azure: az acr build uploads the local build context, builds it on ACR Tasks compute and pushes the tagged image to the registry, so the machine needs only the Azure CLI, no Docker engine.

az acr build --registry fabrikamreg --image orders:2.3.0 .
  • --platform linux/arm64 builds the same Dockerfile for another OS/architecture. A build argument such as TARGETARCH is only an input the Dockerfile can read; it doesn't change the build platform.
  • --no-push builds without pushing, which suits pull-request checks that must not add images.
  • az acr run runs a task once: with --cmd it runs an existing container image, and the final /dev/null context says there is no source to upload. Nothing stays configured afterwards.
az acr run --registry fabrikamreg \
  --cmd '$Registry/jobs/seed-data:1.2' /dev/null

Inside a run, $Registry expands to the registry's login server (for example fabrikamreg.azurecr.io); $RegistryName is only the short name. az acr task create, by contrast, creates a reusable task resource with triggers; it doesn't run anything by itself until triggered or started with az acr task run.

How do ACR task triggers work?

An ACR task created with az acr task create can run automatically on three triggers: a source commit (or pull request) in a Git repository, a timer, and a base image update.

Source commit

A task whose --context is a GitHub or Azure Repos URL (for example https://github.com/contoso/api.git#main) gets a commit trigger; pass --git-access-token with a personal access token so ACR can create the repository webhook.

Timer

--schedule takes a five-field cron expression in UTC: minute, hour, day of month, month, day of week. "30 4 * * Mon" is 04:30 UTC every Monday; there is no seconds field. Convert local times yourself: 07:00 in a UTC−5 city is "0 12 * * *" (12:00 UTC), and 06:00 in a UTC+9 city is "0 21 * * *" (21:00 UTC the previous day). A task can hold several named timers: az acr task timer add --timer-name midweek --schedule "0 22 * * Wed" adds one while az acr task timer update replaces a named timer's schedule. A timer-only maintenance task uses --context /dev/null.

Base image update

The base image trigger is enabled by default, and its rules are precise:

  • ACR Tasks discovers base image dependencies while it builds, so the task must have run once before any update can trigger it.
  • It tracks only the base of the application (runtime) image, the last stage of a multi-stage Dockerfile, not intermediate build-stage images such as an SDK image.
  • It fires when the image behind the referenced tag changes, so the Dockerfile must reference a stable tag (node:20) that the platform team updates. Publishing each patch as a new tag (20.11.2) never changes a pinned 20.11.1.
  • Base images in an Azure container registry trigger immediately; base images in public repositories such as Docker Hub or MCR are polled at a random interval of 10 to 60 minutes.

How do you write a multi-step ACR task in YAML?

A multi-step task is a YAML file of steps, each of type build (docker build), push (push images) or cmd (run a container), and the when property controls ordering: without it a step waits for the step before it, when: ["-"] starts it immediately in parallel, and when: ["a", "b"] makes it wait for every named step.

version: v1.1.0
steps:
  - id: build-svc
    build: -t $Registry/svc:$ID ./svc
  - id: unit
    cmd: $Registry/svc:$ID npm test
    when: ["build-svc"]
  - id: lint
    cmd: $Registry/svc:$ID npm run lint
    when: ["build-svc"]
  - push: ["$Registry/svc:$ID"]
    when: ["unit", "lint"]

Here the build runs first, the two checks run in parallel because each names only the build, and the push waits for both checks. If a step fails, the run fails and later dependent steps never execute, which is how "push only if tests pass" is expressed: make the push depend on the test step. Listing only the build (or only one check) lets the push race the remaining check.

From version v1.1.0, predefined aliases can be used instead of run variables such as {{.Run.Registry}}, which still work: $Registry (login server of the run registry, so one file works in dev and prod), $RegistryName (short name), $ID (run ID, a convenient unique tag) and others such as $Branch. An image name without a registry prefix refers to Docker Hub.

Secrets stay out of the file and the CLI through a secrets block: each entry has an id and a keyvault secret URL (plus clientID for a user-assigned identity), and steps reference the value as {{.Secrets.<id>}}. The task authenticates to Key Vault with its own managed identity, which needs a data plane role such as Key Vault Secrets User; Key Vault Reader can't read secret values, and the creator's own rights don't apply at run time.

How does App Service pull and deploy a custom container?

App Service for Linux pulls a custom container from Azure Container Registry most securely with a managed identity: grant the identity AcrPull on the registry and set the site property acrUseManagedIdentityCreds to true, so no registry user name or password is stored in configuration (the admin user with DOCKER_REGISTRY_SERVER_PASSWORD works but stores a password).

  • By default the pull uses the system-assigned identity. To use a user-assigned identity, set acrUserManagedIdentityID to that identity's client ID. If the app has both identities and only the user-assigned one holds AcrPull, pulls fail until this is set.
  • For a registry with public access disabled behind a private endpoint, the platform's image pull goes through the integrated virtual network only when vnetImagePullEnabled is true (shown in newer API versions and the portal as outboundVnetRouting.imagePullTraffic). Routing the app's own traffic (vnetRouteAllEnabled) doesn't cover the image pull.
  • The managed-identity pull uses ARM-audience tokens, so the registry must allow ARM-audience authentication (see the authentication section).

Continuous deployment

Turning on continuous deployment in Deployment Center adds a webhook to the selected repository in ACR. Each push of the configured image posts to the webhook, which restarts the app so it pulls the updated image. Two consequences follow:

  • The restarted app pulls the image:tag it is configured with. CD by webhook suits a stable tag; with unique tags per build, the pipeline must update the app's image (for example az webapp config container set), and that configuration change itself makes the app pull and start the new container.
  • The webhook authenticates to the app's SCM endpoint with publishing credentials, so it needs Basic Auth Publishing Credentials enabled in the app's platform settings. Security baselines often disable it, which shows up as HTTP 401 in the webhook's delivery log while manual pulls still work.

To test each image on a warmed-up instance before users reach it, point CD at a staging slot and swap it into production. Before routing traffic, the swap applies the production slot's settings to the staging instances and warms them up, so there is no cold start. The image moves with the slot; settings marked as deployment slot settings stay behind.

How do you supply environment variables and secrets to a container on App Service?

App Service injects every app setting into the container process as an environment variable with the same name, so values such as DB_HOST come from configuration instead of being built into the image, and secrets come from Key Vault references placed in app settings.

Platform settings for custom containers

SettingWhat it does
WEBSITES_PORTThe port App Service sends requests and its startup ping to; the default is 80. Set it when the container listens elsewhere, no rebuild needed.
WEBSITES_ENABLE_APP_SERVICE_STORAGEMounts shared persistent storage at /home. It defaults to false for custom Linux containers, so files written there are lost on restart and not shared across instances.
WEBSITES_CONTAINER_START_TIME_LIMITHow long the platform waits for the container to start before giving up. It changes timing only, not the port or storage.

App settings, connection strings and slots

Connection strings are also exposed as environment variables, but with a type prefix such as SQLCONNSTR_, SQLAZURECONNSTR_, MYSQLCONNSTR_, POSTGRESQLCONNSTR_, SERVICEBUSCONNSTR_ or CUSTOMCONNSTR_. A Custom connection string named Inventory is read as CUSTOMCONNSTR_Inventory; code that reads a plain name should use an app setting. Mark a setting as a deployment slot setting (sticky) when each slot must keep its own value after a swap; unmarked settings travel with the swap.

Key Vault references

Set the app setting's value to a reference in one of two forms; the app then reads the secret as an ordinary environment variable:

@Microsoft.KeyVault(SecretUri=https://kv-shop.vault.azure.net/secrets/api-key)
@Microsoft.KeyVault(VaultName=kv-shop;SecretName=api-key)
  • The URI must use the /secrets/ path, and the name form separates parts with semicolons. Omitting the version makes the reference follow the latest version, so rotation needs no setting change; adding a version pins it.
  • Resolved values are cached and refetched every 24 hours. Any configuration change restarts the app and refetches immediately; to refresh without changing configuration, POST to …/config/configreferences/appsettings/refresh. App Service doesn't subscribe to Key Vault events.
  • The identity used is the system-assigned one by default and needs a secret-read role such as Key Vault Secrets User (Key Vault Reader can't read values). To use a user-assigned identity, set keyVaultReferenceIdentity to its resource ID; it applies to every reference in the app. A user-assigned identity can be authorized before the app exists, so references resolve on first creation. Don't confuse it with acrUserManagedIdentityID, which takes a client ID.
  • If a reference can't be resolved, the app sees the literal reference text. For a vault reachable only through a private endpoint, a Linux app with virtual network integration must route all outbound traffic through the virtual network (vnetRouteAllEnabled, or outboundVnetRouting in newer API versions).

Tip. Questions on this topic are scenarios with one deciding requirement, such as no local Docker engine, keep the original digest, least privilege, no stored passwords, a Standard tier registry, without rebuilding the image, or without changing configuration. Many show a CLI command, a cron expression, a task YAML file or a Key Vault reference and ask which variant meets the requirement, so the near-twins matter: write-enabled versus delete-enabled, image versus repository scope, $Registry versus $RegistryName, client ID versus resource ID, vnetImagePullEnabled versus vnetRouteAllEnabled, and app settings versus connection strings.

Key takeaways
  • Geo-replication, private endpoints, IP/VNet rules, the retention policy and dedicated data endpoints are Premium-only; artifact cache rules and tokens work in every tier.
  • Deploy unique tags or digests; stable tags move with every push. write-enabled false makes a tag immutable and undeletable, delete-enabled false only blocks deletion, and az lock never protects images.
  • az acr import is a server-side copy that keeps the digest; a cache rule (with a credential set for Docker Hub) is a pull-through cache.
  • AcrPull pulls, AcrPush pushes and pulls, AcrDelete deletes. In rbac-abac mode those roles aren't honored; use Container Registry Repository roles with an ABAC condition to scope them.
  • az acr build builds without local Docker (--platform, --no-push); az acr run --cmd ... /dev/null runs an image once; $Registry is the login server, $RegistryName the short name.
  • Timer cron has five fields in UTC. Base image triggers need one prior run, a stable tag and the runtime stage; ACR bases trigger immediately, public ones within 10 to 60 minutes.
  • In task YAML, when lists every prerequisite step and ["-"] runs in parallel; secrets come from a secrets block read by the task's own managed identity.
  • App Service pulls with AcrPull plus acrUseManagedIdentityCreds; acrUserManagedIdentityID takes a client ID, keyVaultReferenceIdentity takes a resource ID, and vnetImagePullEnabled routes pulls through the VNet.
  • App settings become environment variables; connection strings get type prefixes; slot settings stay with their slot; Key Vault references cache for 24 hours unless refreshed.

Frequently asked questions

What is the difference between az acr build and az acr import?

az acr build is an ACR Tasks quick task: it uploads a build context and runs a Docker build in Azure, producing a new image with a new digest, without needing Docker on your machine. az acr import copies an existing image from another registry server-side, keeping its original manifest digest, and builds nothing.

Do I need the Premium tier of Azure Container Registry?

Only if you need a Premium feature: geo-replication, private endpoints, IP or virtual network access rules, the retention policy for untagged manifests or dedicated data endpoints. ACR Tasks, tokens and scope maps and image locks are available in Basic and Standard as well, and so are artifact cache rules.

Why doesn't my ACR task rebuild when its base image is updated?

Check four things: the task must have run at least once so it recorded the dependency; only the base of the final runtime stage is tracked, not build-stage images; the Dockerfile must reference a stable tag that actually receives the update rather than a pinned version tag; and public base images are only polled every 10 to 60 minutes, while bases in an Azure registry trigger immediately.

How does App Service pass environment variables to a custom container?

Every app setting is injected into the container as an environment variable with the same name. Connection strings are injected too, but with a type prefix such as CUSTOMCONNSTR_ or SQLAZURECONNSTR_. Settings marked as deployment slot settings stay with their slot during a swap.

How do I use a Key Vault secret in an App Service app setting?

Set the value to @Microsoft.KeyVault(SecretUri=https://<vault>.vault.azure.net/secrets/<name>) or @Microsoft.KeyVault(VaultName=<vault>;SecretName=<name>), and give the app's managed identity a secret-read role such as Key Vault Secrets User. Without a version the reference follows the latest secret version; values are refetched every 24 hours, on any configuration change, or on demand through the config references refresh endpoint.

Why does App Service fail to pull my image with a managed identity?

Common causes are: acrUseManagedIdentityCreds is false; the app uses its system-assigned identity while only a user-assigned identity holds AcrPull, so acrUserManagedIdentityID must be set to that identity's client ID; the registry is in rbac-abac mode, where AcrPull isn't honored; ARM-audience authentication was disabled by policy; or the registry is private and vnetImagePullEnabled is not set.

How do I make an image in Azure Container Registry immutable?

Run az acr repository update with --image <repo>:<tag> --write-enabled false. That blocks both overwriting and deleting the tag. delete-enabled false alone still allows overwrites, and an Azure resource lock created with az lock protects only the registry resource, not its images.

Source

This lesson covers the "Develop containerized solutions on Azure" domain of the official AI-200 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?