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