Pushing Docker Images to Azure Container Registry with GitHub Actions
Set up a GitHub Actions workflow to build and push Docker images to ACR. Spoiler: the OIDC token approach doesn't work with buildx. Here's what does.
I needed to build 4 Docker images from a monorepo and push them to Azure Container Registry on every push to main. Sounds simple. It wasn't.
What I was going for
Four services, four images, all pushed to wira360.azurecr.io automatically. Tags with the git SHA for traceability, plus latest for convenience. No stored secrets — OIDC for auth.
The workflow (final version)
name: Build and Push to ACR
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-push:
name: ${{ matrix.service }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- service: sokoni-api
dockerfile: products/sokoni/api/Dockerfile
- service: sokoni-web
dockerfile: products/sokoni/web/Dockerfile
- service: payments-api
dockerfile: services/payments/api/Dockerfile
- service: admin-web
dockerfile: platform/admin/web/Dockerfile
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to ACR
uses: docker/login-action@v3
with:
registry: wira360.azurecr.io
username: \${{ secrets.ACR_USERNAME }}
password: \${{ secrets.ACR_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: \${{ matrix.dockerfile }}
push: true
tags: |
wira360.azurecr.io/\${{ matrix.service }}:\${{ github.sha }}
wira360.azurecr.io/\${{ matrix.service }}:latest
cache-from: type=registry,ref=wira360.azurecr.io/\${{ matrix.service }}:cache
cache-to: type=registry,ref=wira360.azurecr.io/\${{ matrix.service }}:cache,mode=maxfail-fast: false means a slow build in one job won't cancel the others. The registry cache means repeat builds only transfer changed layers.
One-time Azure setup
Before this can run you need an ACR. I already had one at wira360.azurecr.io. Enable admin credentials:
az acr update --name wira360 --admin-enabled true
az acr credential show --name wira360That gives you a username and password. Store them as GitHub secrets:
ACR_USERNAMEACR_PASSWORD
The OIDC rabbit hole
I originally wanted keyless auth — no stored secrets, just OIDC. The setup looks clean: create an app registration, add a federated credential pointing at your repo, grant it AcrPush, and use azure/login@v2 in the workflow.
az ad app create --display-name "github-tracker-acr"
az ad sp create --id <appId>
az ad app federated-credential create \
--id <appId> \
--parameters '{
"name": "github-tracker-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:eduuh/wira360:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
ACR_ID=$(az acr show --name wira360 --query id -o tsv)
az role assignment create --assignee <appId> --role AcrPush --scope $ACR_IDThe audience must be api://AzureADTokenExchange. If you use api://AzureADTokenEndpoint (which looks plausible), OIDC silently fails with AADSTS700212.
OIDC login worked. But the Docker push kept failing with insufficient_scope: authorization failed.
The problem: after azure/login@v2 authenticates you, the next step is getting Docker credentials for the registry. The obvious approach is az acr login --name wira360, which writes credentials to the default Docker context. Except buildx runs in its own container driver and doesn't inherit those credentials.
So you try az acr login --expose-token, get a short-lived access token, and pass it to docker login. Closer, but still fails. The reason: buildx's OAuth challenge flow with ACR expects either basic auth credentials or a refresh token. A bare access token doesn't satisfy the challenge, so ACR returns insufficient_scope.
There are ways to make OIDC work with buildx (passing the token through buildx secrets, using a different driver), but they add complexity for marginal benefit. Admin credentials stored as GitHub secrets are static, they work, and that's enough.
Verifying it worked
az acr repository list --name wira360
az acr repository show-tags --name wira360 --repository sokoni-apiYou should see your git SHAs listed as tags.
OIDC is elegant in theory. Admin credentials are boring and they work.
Last updated on February 22nd, 2026