ACR Tasks: Build Container Images in the Cloud Without Docker

TQ
Tran Quang
September 17, 2026 · 12 min read
#Azure Container Registry#ACR Tasks#Docker#CI/CD

Sometimes you need a container image in Azure Container Registry but you have no Docker to build it with: a locked-down corporate laptop, an Arm64 machine building for x64, a Codespace without Docker-in-Docker, or a pipeline agent where running a privileged daemon is not allowed. Other times the problem is maintenance: you want images rebuilt automatically when Microsoft patches the .NET base image, without anyone remembering to do it.

ACR Tasks solves both. It is a build service built into the registry: you send it a source context and a Dockerfile, it builds on Azure-managed compute and pushes the result straight into the registry. This guide walks through quick builds, triggered tasks, multi-step YAML, operations and identity, and ends with an honest comparison against GitHub Actions and Azure Pipelines. If you are new to registries, read Introduction to Azure Container Registry first.

What ACR Tasks is and how it runs

There are three ways to use ACR Tasks:

  • Quick tasks: az acr build uploads your local source and builds it once. Think of it as docker build && docker push executed in Azure.
  • Triggered tasks: az acr task create defines a named task that runs on a git commit, a pull request, a base image update, or a timer.
  • Multi-step tasks: a YAML file (by convention acr-task.yaml) that chains build, push and arbitrary container commands, run with az acr run or as a named task.

Each run executes in an isolated, ephemeral environment. The registry the task belongs to is authenticated automatically, so pushing to it needs no credentials in your definition. Runs are billed by compute time; check the current pricing page rather than relying on figures in blog posts.

The admin user plays no part in any of this. Keep it disabled (az acr update -n crappsdemo --admin-enabled false): it is a single shared password with full push rights and no per-caller audit trail, and ACR Tasks authenticates with Entra ID and managed identities instead.

Quick builds with az acr build

From the repository root:

bash
az acr build \
  --registry crappsdemo \
  --image orders-api:{{.Run.ID}} \
  --image orders-api:latest \
  --file src/Orders.Api/Dockerfile \
  .

The CLI packs the context directory (respecting .dockerignore), uploads it, queues a run and streams the log back to your terminal. {{.Run.ID}} is replaced by the run identifier, such as cf1, which gives each build a unique tag without scripting. You can pass --image several times to apply multiple tags.

Useful flags:

bash
# build for Arm64 regardless of your machine's architecture
az acr build -r crappsdemo -t orders-api:arm64-{{.Run.ID}} --platform linux/arm64 .

# pass build arguments
az acr build -r crappsdemo -t orders-api:{{.Run.ID}} --build-arg BUILD_CONFIGURATION=Release .

# build a remote git repo instead of a local folder
az acr build -r crappsdemo -t orders-api:{{.Run.ID}} \
  https://github.com/my-org/orders-api.git#main:src

# queue and return immediately; check logs later
az acr build -r crappsdemo -t orders-api:{{.Run.ID}} --no-logs --no-wait .

Keep .dockerignore tight. Every quick build uploads the context, so a forgotten bin/, obj/, node_modules/ or .git/ directory turns a few-second upload into minutes. Note also that --image (alias -t) expects a repository and tag only; the registry login server is prefixed for you.

Quick builds require permission to schedule runs on the registry (Microsoft.ContainerRegistry/registries/scheduleRun/action). AcrPush alone does not include it. Check the ACR built-in roles documentation for a role that covers task runs, or scope Contributor to the registry only.

Triggered tasks on git commits

A named task watches a repository and builds on every commit to a branch. ACR registers a webhook on the repository, so it needs a personal access token that can read the repo and manage webhooks (for GitHub, the repo and admin:repo_hook scopes, or a fine-grained token with equivalent permissions).

bash
az acr task create \
  --registry crappsdemo \
  --name build-orders-api \
  --image orders-api:{{.Run.ID}} \
  --context https://github.com/my-org/orders-api.git#main \
  --file src/Orders.Api/Dockerfile \
  --git-access-token $GIT_PAT \
  --commit-trigger-enabled true \
  --pull-request-trigger-enabled false

The context URL format is https://<repo>.git#<branch>:<folder>, where the branch and folder are optional. Keep pull request triggers off for public repositories: a PR-triggered task would build untrusted code with a task that can push to your registry.

Two practical notes. The token is stored with the task, so when it expires the webhook keeps firing but runs fail to clone; renew with az acr task update --name build-orders-api --registry crappsdemo --git-access-token $NEW_PAT. And a commit trigger builds every push, including documentation-only changes, so large monorepos may prefer a CI system with path filters.

Base image update triggers

This is the feature that justifies ACR Tasks even for teams that build elsewhere. When a task builds an image, ACR records the base images it used. If a base image is updated later, for example when a new patch of mcr.microsoft.com/dotnet/aspnet:10.0 is published, ACR can re-run the task automatically, so your application image picks up OS and runtime security fixes without a code change.

bash
az acr task create \
  --registry crappsdemo \
  --name rebuild-on-base \
  --image orders-api:{{.Run.ID}} \
  --context https://github.com/my-org/orders-api.git#main \
  --file src/Orders.Api/Dockerfile \
  --git-access-token $GIT_PAT \
  --base-image-trigger-enabled true \
  --base-image-trigger-type Runtime

Things to know:

  • Tracking works for base images in the same registry, in another ACR, and for public images on Docker Hub and MCR. Detection for public registries is not instantaneous.
  • Runtime triggers on changes to the final-stage base image; All also includes build-stage images. For a .NET multi-stage build, Runtime is normally what you want, because SDK updates do not change what ships.
  • The task must have run at least once so ACR knows the dependencies. Run it manually after creating it.
  • A common production pattern is to import the public base image into your own registry (a base/ repository) on a schedule, test it, and have application tasks trigger on that curated copy. You control when patches flow and are insulated from upstream rate limits and outages.

The trigger rebuilds; it does not deploy. Pair it with something that notices new tags, or at least with an image scanner and a deployment process that picks up the rebuilt digest.

Timer and scheduled tasks

Tasks can also run on a cron schedule, in UTC. Scheduled tasks are useful for nightly rebuilds, periodic maintenance commands, or imports. A task with no source context uses /dev/null:

bash
az acr task create \
  --registry crappsdemo \
  --name nightly-hello \
  --context /dev/null \
  --cmd mcr.microsoft.com/hello-world \
  --schedule "0 2 * * *"

Add, change or remove timers on an existing task:

bash
az acr task timer add    -r crappsdemo -n build-orders-api --timer-name weekly --schedule "0 3 * * 1"
az acr task timer list   -r crappsdemo -n build-orders-api -o table
az acr task timer remove -r crappsdemo -n build-orders-api --timer-name weekly

A task can have several timers alongside commit and base image triggers. The most common real use is cleanup: running acr purge on a schedule to delete old tags, covered in the retention and purge guide linked below.

Multi-step tasks with acr-task.yaml

Single-image builds are fine, but real pipelines need to build several images, run tests in a container, and push only if the tests pass. Multi-step tasks describe this in YAML:

yaml
version: v1.1.0
stepTimeout: 1200
steps:
  - id: build-api
    build: >
      -t $Registry/orders-api:{{.Run.ID}}
      -t $Registry/orders-api:latest
      -f src/Orders.Api/Dockerfile .

  - id: build-tests
    build: -t orders-api-tests:{{.Run.ID}} -f tests/Dockerfile .
    when: ["-"]

  - id: run-tests
    cmd: orders-api-tests:{{.Run.ID}}
    when: ["build-api", "build-tests"]

  - id: push
    push:
      - $Registry/orders-api:{{.Run.ID}}
      - $Registry/orders-api:latest
    when: ["run-tests"]

How it works:

  • build takes the same arguments as docker build. $Registry is an alias for the registry's login server (crappsdemo.azurecr.io).
  • cmd runs a container. If it exits non-zero, the run fails and later steps are skipped, which is how tests gate the push. The test image is never pushed because it is not in the push list.
  • when: ["-"] starts a step immediately, in parallel with others; when: ["build-api", "build-tests"] waits for those step IDs. Without when, steps run sequentially.
  • Other useful template values include {{.Run.Registry}}, {{.Run.Commit}} and {{.Run.Branch}} for triggered runs.

Run it once against local source, or register it as a triggered task:

bash
az acr run --registry crappsdemo -f acr-task.yaml .

az acr task create \
  --registry crappsdemo \
  --name orders-pipeline \
  --context https://github.com/my-org/orders-api.git#main \
  --file acr-task.yaml \
  --git-access-token $GIT_PAT

cmd steps can also run tools such as the Azure CLI or Helm images, and a step can be detach: true to run a background container, for example a database for integration tests. Keep that in proportion, though: once the YAML grows into a full pipeline with approvals and environments, a CI system is the better home.

Running, watching and debugging runs

bash
# trigger a named task manually
az acr task run --registry crappsdemo --name build-orders-api

# list recent runs with status, trigger and duration
az acr task list-runs --registry crappsdemo --top 10 -o table

# stream or replay the log of a specific run
az acr task logs --registry crappsdemo --run-id cf1

# details of a run, including the digests it produced
az acr task show-run --registry crappsdemo --run-id cf1

# cancel a stuck run
az acr task cancel-run --registry crappsdemo --run-id cf1

list-runs shows the trigger type (Manual, Commit, Image Update, Timer), so when an unexpected image appears you can tell which trigger produced it.

Symptom Likely cause Fix
Commit trigger does not fire Webhook missing or PAT lacks hook permission Recreate the task with a valid PAT; check repository webhook deliveries
Run fails at source clone PAT expired or revoked az acr task update --git-access-token
Base image update never triggers Task never ran, or trigger type excludes the changed stage Run the task manually once; check --base-image-trigger-type
denied pulling from another registry No credential or identity for that registry Add a managed identity and az acr task credential add
Run cannot reach a network-restricted registry Tasks run on shared compute outside your VNet Allow trusted services with a system-assigned identity, or look at dedicated agent pools
AuthorizationFailed on scheduleRun/action Caller has AcrPush only Grant a role that includes task runs

Managed identity for tasks

A task pushes to its own registry automatically, but anything else, such as pulling a base image from a shared registry, pushing to a second registry, or reading a Key Vault secret, needs an identity. Assign one when creating the task:

bash
# system-assigned identity
az acr task create -r crappsdemo -n build-orders-api ... --assign-identity

# or a user-assigned identity
az acr task create -r crappsdemo -n build-orders-api ... \
  --assign-identity /subscriptions/<sub-id>/resourceGroups/rg-apps/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-acr-tasks

Grant the identity AcrPull on a registry it reads from, then tell the task to use it for that login server:

bash
az acr task credential add \
  --registry crappsdemo \
  --name build-orders-api \
  --login-server crsharedbase.azurecr.io \
  --use-identity [system]

For a user-assigned identity, pass its client ID to --use-identity. In multi-step YAML, secrets can come from Key Vault through the identity instead of being embedded in the task:

yaml
version: v1.1.0
secrets:
  - id: nugetToken
    keyvault: https://kv-apps.vault.azure.net/secrets/nuget-feed-token
steps:
  - build: -t $Registry/orders-api:{{.Run.ID}} --build-arg NUGET_TOKEN={{.Secrets.nugetToken}} .

Build arguments are visible in image history; prefer BuildKit secret mounts where your build supports them, and treat this pattern as a way to keep secrets out of the task definition, not out of the image.

If the registry has public access disabled, runs need the registry's trusted services exception (az acr update -n crappsdemo --allow-trusted-services true) and a system-assigned identity on the task. Dedicated agent pools that run inside your VNet exist for Premium registries; check the current docs for their availability and limitations.

When to use ACR Tasks versus GitHub Actions or Azure Pipelines

Need ACR Tasks GitHub Actions / Azure Pipelines
Build without a local Docker daemon Strong fit (az acr build) Needs a runner with Docker or Buildx
Rebuild when a base image is patched Built in Must be scripted or scheduled
Scheduled maintenance (purge, imports) Built in, runs next to the registry Possible with scheduled workflows
Tests, approvals, environments, PR checks Basic (cmd steps) Rich and well integrated
Layer cache between builds Limited control GitHub cache or registry cache via Buildx
Path filters and monorepo logic Not available on commit triggers Available
Visibility for developers Azure CLI and portal In the pull request

In practice the two combine well. Build and test in your CI system, where developers already look, and push with OIDC. Use ACR Tasks for what it is uniquely good at: base image update rebuilds, scheduled purge and import jobs, and quick one-off builds from machines without Docker.

Conclusion

ACR Tasks moves image builds to where the images live. az acr build replaces docker build and docker push on any machine with the Azure CLI. Named tasks add commit, base image and timer triggers, and multi-step YAML chains builds, test containers and pushes with {{.Run.ID}} tags. Give tasks a managed identity when they touch anything beyond their own registry, keep git tokens renewed, and use list-runs and logs to see what ran and why. Treat it as a complement to your CI system rather than a replacement: the base image trigger alone is worth setting up, because it keeps shipped images patched without anyone having to remember.

Found this useful?

Share it with someone who might need it.

Related articles All articles