The Challenge
When managing containerized applications in Azure Container Apps, you eventually face the question: should you continue using Docker Hub, or migrate to Azure Container Registry (ACR)? For enterprise workloads, ACR offers significant advantages: integrated security, managed identities for authentication, reduced egress costs, and better integration with Azure RBAC.
This post documents a complete migration from Docker Hub to ACR across multiple Container Apps in different Azure subscriptions, including the critical managed identity configuration that enables seamless authentication without storing credentials.
Architecture Overview
Our setup consisted of:
- Container Apps in two subscriptions (dev and prod)
- Azure Container Registry centralized in the prod subscription
- GitHub Actions workflows for CI/CD
- Multiple applications (.NET 5.0 and .NET 8.0) requiring migration
The key challenge: enabling Container Apps in the dev subscription to pull images from ACR in the prod subscription without hardcoded credentials.
Step 1: Provisioning Azure Container Registry
First, create the ACR in your centralized subscription:
az account set --subscription "PROD-SUBSCRIPTION"
az acr create \
--name mycontainerreg \
--resource-group rg-container-registry \
--sku Standard \
--location centralus \
--admin-enabled false
Note the --admin-enabled false flag. We’re deliberately avoiding admin credentials in favor of managed identities.
Step 2: Enabling Managed Identities on Container Apps
Each Container App needs a system-assigned managed identity:
# For a Container App in the dev subscription
az account set --subscription "DEV-SUBSCRIPTION"
az containerapp identity assign \
--name my-container-app-dev \
--resource-group my-containers-dev \
--system-assigned
Verify the identity was created:
PRINCIPAL_ID=$(az containerapp show \
--name my-container-app-dev \
--resource-group my-containers-dev \
--query identity.principalId \
-o tsv)
echo $PRINCIPAL_ID
# Output: 8a8a9b91-3f9b-4914-bdb3-c7fd618db167
Step 3: Cross-Subscription Role Assignments
This is where it gets interesting. The Container App in the dev subscription needs permission to pull from ACR in the prod subscription.
# Switch to the prod subscription where ACR lives
az account set --subscription "PROD-SUBSCRIPTION"
ACR_ID="/subscriptions/<prod-sub-id>/resourceGroups/rg-container-registry/providers/Microsoft.ContainerRegistry/registries/mycontainerreg"
# Grant AcrPull role to the dev Container App's managed identity
az role assignment create \
--assignee $PRINCIPAL_ID \
--role AcrPull \
--scope $ACR_ID
The AcrPull role is specifically designed for this use case—it allows pulling images but not pushing or administrative actions.
Step 4: Configuring Container App Registry Authentication
Now configure the Container App to use its managed identity for ACR authentication:
# Switch back to dev subscription
az account set --subscription "DEV-SUBSCRIPTION"
az containerapp registry set \
--name my-container-app-dev \
--resource-group my-containers-dev \
--server mycontainerreg.azurecr.io \
--identity system
This tells the Container App: “When you need to pull images from mycontainerreg.azurecr.io, use your system-assigned managed identity.”
Step 5: Updating GitHub Actions Workflows
Docker Build and Push
Update your workflow to push to ACR instead of Docker Hub:
build-container:
runs-on: ubuntu-latest
env:
DOCKER_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build container image
run: docker compose -f ./deployment/docker/docker-compose.yml build
shell: bash
- uses: docker/login-action@v3
with:
registry: ${{ secrets.ACR_LOGIN_SERVER }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Push docker image to ACR
run: docker push ${{ secrets.ACR_LOGIN_SERVER }}/${{ github.event.repository.name }}:${{ env.DOCKER_TAG }}
shell: bash
Container App Deployment
The critical addition here is the acrName parameter:
production-deploy:
runs-on: ubuntu-latest
env:
DOCKER_TAG: ${{ github.sha }}
needs: build-container
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: '{
"clientId": "${{secrets.AZURE_CLIENT_ID}}",
"clientSecret": "${{secrets.AZURE_CLIENT_SECRET}}",
"subscriptionId": "${{secrets.AZURE_SUBSCRIPTION_PROD}}",
"tenantId": "${{secrets.AZURE_TENANT}}"}'
- name: Build and deploy Container App
uses: azure/container-apps-deploy-action@v1
with:
acrName: mycontainerreg # CRITICAL: Enables ACR authentication
containerAppName: my-container-app-prod
resourceGroup: my-containers-prod
imageToDeploy: ${{ secrets.ACR_LOGIN_SERVER }}/${{github.event.repository.name}}:${{env.DOCKER_TAG}}
containerAppEnvironment: 'Prod'
environmentVariables: 'VAULT_ROLE_ID=secretref:vault-role-id
VAULT_SECRET_ID=secretref:vault-secret-id
CRDS_ENV=prod'
Why acrName matters: Without this parameter, the Container Apps deployment action can’t properly authenticate to ACR using the Azure login credentials. This parameter bridges the gap between your service principal and ACR.
Step 6: Updating docker-compose.yml Files
A subtle but critical issue: your docker-compose.yml must build with the ACR image tag, not Docker Hub:
Before:
services:
app:
image: dockerhub-org/my-app:${DOCKER_TAG:-local}
build:
context: ../../
dockerfile: ./deployment/docker/Dockerfile
After:
services:
app:
image: mycontainerreg.azurecr.io/my-app:${DOCKER_TAG:-local}
build:
context: ../../
dockerfile: ./deployment/docker/Dockerfile
Without this change, Docker builds the image with the wrong tag, and the subsequent push fails with:
An image does not exist locally with the tag: mycontainerreg.azurecr.io/my-app
Step 7: Service Principal Permissions for GitHub Actions
Your GitHub Actions service principal needs AcrPull permission as well (for pushing during builds):
az account set --subscription "PROD-SUBSCRIPTION"
az role assignment create \
--assignee <service-principal-app-id> \
--role AcrPull \
--scope $ACR_ID
This service principal is typically shared across all your workflows (dev, int, prod), so a single role assignment covers all environments.
Special Case: Debian Buster Archives
If you’re using older .NET versions (like .NET 5.0) that depend on Debian Buster, you’ll hit a critical issue: Debian Buster was archived in August 2024. Your Dockerfile will fail with:
404 Not Found [IP: 151.101.xxx.xxx 80]
Solution: Update your Dockerfile to point to the Debian archive:
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim
# Install wget
RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list \
&& sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& sed -i '/buster-updates/d' /etc/apt/sources.list \
&& apt-get update \
&& apt-get install -y wget
# Install gnupg
RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list \
&& sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& sed -i '/buster-updates/d' /etc/apt/sources.list \
&& apt-get install -y gnupg
# Install New Relic (or other apt packages)
RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list \
&& sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& sed -i '/buster-updates/d' /etc/apt/sources.list \
&& echo 'deb http://apt.newrelic.com/debian/ newrelic non-free' | tee /etc/apt/sources.list.d/newrelic.list \
&& wget -O- https://download.newrelic.com/548C16BF.gpg | apt-key add - \
&& apt-get update \
&& apt-get install newrelic-netcore20-agent
Important: Apply these sed commands before every apt-get command that needs to access Debian repositories. The sources.list gets reset with certain operations.
Note: .NET 8.0 and later don’t need these fixes—they use newer base images that aren’t affected.
GitHub Secrets Configuration
Set these organization-level secrets in GitHub:
ACR_LOGIN_SERVER: mycontainerreg.azurecr.io
ACR_USERNAME: <token-name>
ACR_PASSWORD: <token-password>
For the password, generate an ACR token:
az acr token create \
--name github-push-token \
--registry mycontainerreg \
--scope-map _repositories_push
# Get the password
az acr token credential generate \
--name github-push-token \
--registry mycontainerreg
Critical: Store the plain text password in GitHub Secrets, not base64-encoded.
Verification and Troubleshooting
Checking Managed Identity Configuration
Verify a Container App is configured correctly:
az containerapp show \
--name my-container-app-dev \
--resource-group my-containers-dev \
--query "{identity: identity, registries: properties.configuration.registries}" \
-o json
Expected output:
{
"identity": {
"principalId": "8a8a9b91-3f9b-4914-bdb3-c7fd618db167",
"type": "SystemAssigned"
},
"registries": [
{
"identity": "system",
"server": "mycontainerreg.azurecr.io"
}
]
}
Common Issues
Issue: Container App stuck in “Activating” state
Cause: Usually indicates the container can’t start, for me this was due to New Relic agent path mismatch that I missed:
Solution: Check console logs:
az containerapp logs show \
--name my-container-app-prod \
--resource-group my-containers-prod \
--tail 50
Issue: “An image does not exist locally with the tag”
Cause: docker-compose.yml building with Docker Hub tag, workflow pushing to ACR tag.
Solution: Update docker-compose.yml image reference to ACR format.
Issue: “AuthorizationFailed” during deployment
Cause: Missing role assignments or wrong scope.
Solution: Verify role assignments:
az role assignment list \
--assignee $PRINCIPAL_ID \
--scope $ACR_ID \
-o table
Migration Checklist
- Create ACR in centralized subscription
- Enable system-assigned managed identity on each Container App
- Grant AcrPull role to each managed identity (cross-subscription if needed)
- Configure Container App registry authentication with
--identity system - Grant AcrPull role to GitHub Actions service principal
- Update GitHub Actions workflows:
- Change docker login to ACR
- Update image push commands
- Add
acrNameparameter to deployment steps
- Update docker-compose.yml files with ACR image tags
- Apply Debian Buster archive fixes (if using .NET 5.0 or older)
- Test deployment in dev environment
- Apply same changes to prod
- Remove Docker Hub credentials after successful migration
Performance and Cost Benefits
After migration, we observed:
- Reduced latency: Image pulls from ACR in the same region are significantly faster than Docker Hub
- Lower egress costs: No charges for traffic within Azure regions
- Better reliability: No Docker Hub rate limiting issues
- Improved security: No stored credentials, all authentication via managed identities
- Simplified compliance: All container images under organizational control
Conclusion
Migrating from Docker Hub to Azure Container Registry for Container Apps involves several moving parts, but the security and operational benefits are substantial. The key insight is leveraging managed identities for authentication—eliminating credential management while enabling secure cross-subscription access.
The most critical configuration points are:
- Proper role assignments with the correct scope
- The
acrNameparameter in deployment actions - Matching image tags between build and push operations
- Handling deprecated base images (Debian Buster)
With these pieces in place, you get a fully integrated, secure container deployment pipeline that leverages Azure’s native authentication mechanisms.