Back to blog
AI Systemsintermediate

Container Registries: ACR and ECR

Learn how to store, tag, scan, and distribute Docker images using Azure Container Registry and Amazon ECR — including a complete push workflow, image scanning, and geo-replication.

Asma Hafeez KhanMay 15, 202610 min read
LLMOpsDockerACRECRAzureAWSDevOpsCI/CD
Share:𝕏

What Is a Container Registry?

A container registry is a storage and distribution system for Docker images. Think of it as Git for container images — a central repository where you push images from CI and pull them in your container runtime.

Without a registry, you're stuck running docker save | docker load to move images between machines — which is impractical for any team or automated deployment pipeline.

There are three categories of registries:

| Type | Examples | Use case | |---|---|---| | Public cloud registries | Azure ACR, Amazon ECR, Google Artifact Registry | Production workloads, private images | | Public open registries | Docker Hub, GitHub Container Registry | Open-source images, base images | | Self-hosted registries | Harbor, JFrog Artifactory | Air-gapped environments, maximum control |

For most enterprise AI services, you'll use either Azure Container Registry (ACR) or Amazon ECR, often both (multi-cloud deployments). This lesson covers both.


Azure Container Registry (ACR)

ACR is Microsoft's managed container registry service, tightly integrated with Azure services: Azure Container Apps, AKS, Azure DevOps, and GitHub Actions with Azure credentials.

Creating an ACR

Bash
# Variables
RESOURCE_GROUP="rg-pharmabot-prod"
REGISTRY_NAME="pharmabotprod"   # Must be globally unique, 5-50 alphanumeric chars
LOCATION="northeurope"
SKU="Basic"   # Basic | Standard | Premium

# Create resource group if it doesn't exist
az group create --name $RESOURCE_GROUP --location $LOCATION

# Create the registry
az acr create \
  --resource-group $RESOURCE_GROUP \
  --name $REGISTRY_NAME \
  --sku $SKU \
  --admin-enabled false   # Use service principals, not admin credentials

ACR SKU comparison:

| SKU | Storage | Geo-replication | Content Trust | Webhooks | |---|---|---|---|---| | Basic | 10 GB | No | No | No | | Standard | 100 GB | No | No | Yes | | Premium | 500 GB | Yes | Yes | Yes |

Use Standard for most production workloads. Upgrade to Premium only if you need geo-replication (for multi-region deployments) or Content Trust (image signing).

Authenticating to ACR

There are three ways to authenticate. Choose based on your scenario:

Option 1: Service Principal (CI/CD pipelines, production)

Bash
# Create a service principal with acrpush role
SP_PASSWORD=$(az ad sp create-for-rbac \
  --name "sp-pharmabot-acr" \
  --role acrpush \
  --scopes /subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ContainerRegistry/registries/$REGISTRY_NAME \
  --query password \
  --output tsv)

SP_APP_ID=$(az ad sp show \
  --id "http://sp-pharmabot-acr" \
  --query appId \
  --output tsv)

echo "App ID: $SP_APP_ID"
echo "Password: $SP_PASSWORD"
# Save these as GitHub Secrets or Key Vault secrets

# Login with service principal
docker login $REGISTRY_NAME.azurecr.io \
  --username $SP_APP_ID \
  --password $SP_PASSWORD

Option 2: Azure CLI (local development)

Bash
# If you're already logged in with az login, this works automatically
az acr login --name $REGISTRY_NAME

This command fetches a short-lived token from Azure AD and configures your local Docker daemon to use it. No password required.

Option 3: Managed Identity (Azure-hosted workloads)

When running in Azure Container Apps, AKS, or any Azure service with a managed identity, assign the AcrPull role to the identity and the registry integration happens automatically — no credentials to manage.

Bash
# Assign AcrPull to a Container App's system-assigned identity
az role assignment create \
  --assignee <MANAGED_IDENTITY_OBJECT_ID> \
  --role AcrPull \
  --scope /subscriptions/.../registries/$REGISTRY_NAME

Complete Push Workflow for ACR

Here is the complete end-to-end workflow from local build to registry:

Bash
REGISTRY="pharmabotprod.azurecr.io"
IMAGE_NAME="pharmabot"
GIT_SHA=$(git rev-parse --short HEAD)
TAG="${GIT_SHA}-$(date +%Y%m%d)"

# Step 1: Login
az acr login --name pharmabotprod

# Step 2: Build the image with the full registry path as the tag
docker build -t $REGISTRY/$IMAGE_NAME:$TAG .

# Step 3: Also tag as a named release (but NOT :latest  see next section)
docker tag $REGISTRY/$IMAGE_NAME:$TAG $REGISTRY/$IMAGE_NAME:release-1.4.2

# Step 4: Push both tags
docker push $REGISTRY/$IMAGE_NAME:$TAG
docker push $REGISTRY/$IMAGE_NAME:release-1.4.2

Alternative: az acr build — build in the cloud

Instead of building locally and pushing, you can build directly in ACR. This is useful in CI/CD when your build agent doesn't have Docker installed, or when you want to offload the build compute to Azure:

Bash
az acr build \
  --registry pharmabotprod \
  --image pharmabot:$GIT_SHA \
  --file Dockerfile \
  .

az acr build sends your build context to ACR, which runs the Docker build in the cloud and stores the resulting image. No local Docker daemon required.


Amazon ECR

ECR is AWS's managed container registry, integrated with ECS, EKS, Fargate, Lambda, and CodePipeline.

Creating an ECR Repository

Bash
AWS_REGION="eu-west-1"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REPO_NAME="pharmabot"

# Create the repository
aws ecr create-repository \
  --repository-name $REPO_NAME \
  --region $AWS_REGION \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=AES256

echo "Registry URI: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REPO_NAME"

Authenticating to ECR

ECR uses short-lived tokens (valid for 12 hours):

Bash
# Get an auth token and pass it to docker login
aws ecr get-login-password --region $AWS_REGION | \
  docker login \
  --username AWS \
  --password-stdin \
  $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

Pushing to ECR

Bash
REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
GIT_SHA=$(git rev-parse --short HEAD)

# Build
docker build -t $REGISTRY/$REPO_NAME:$GIT_SHA .

# Push
docker push $REGISTRY/$REPO_NAME:$GIT_SHA

ECR Lifecycle Policies

ECR doesn't automatically delete old images. Without a lifecycle policy, old images accumulate and storage costs grow. Add a policy to keep only the last 30 images:

Bash
aws ecr put-lifecycle-policy \
  --repository-name $REPO_NAME \
  --lifecycle-policy-text '{
    "rules": [
      {
        "rulePriority": 1,
        "description": "Keep last 30 images",
        "selection": {
          "tagStatus": "tagged",
          "tagPrefixList": ["release-"],
          "countType": "imageCountMoreThan",
          "countNumber": 30
        },
        "action": {"type": "expire"}
      },
      {
        "rulePriority": 2,
        "description": "Delete untagged images after 7 days",
        "selection": {
          "tagStatus": "untagged",
          "countType": "sinceImagePushed",
          "countUnit": "days",
          "countNumber": 7
        },
        "action": {"type": "expire"}
      }
    ]
  }'

Image Tagging: Never Use :latest in Production

:latest is the default tag when you don't specify one. It's also the most dangerous tag in production.

Why :latest is dangerous:

  1. No immutability. pharmabot:latest today is not the same as pharmabot:latest tomorrow. A rolling restart in your cluster could pull a different image on different nodes.
  2. No traceability. When something goes wrong at 2am, you can't answer "what code is running?" if the tag is :latest.
  3. No rollback. You can't roll back to a previous :latest. The old image may still be in the registry, but you've lost the reference.

Correct tagging strategy:

Bash
GIT_SHA=$(git rev-parse --short HEAD)      # e.g., a3b4c5d
BUILD_DATE=$(date +%Y%m%d)                 # e.g., 20260515
SEMVER="1.4.2"                             # from your release process

# Primary tag: git SHA (immutable, traceable)
docker tag pharmabot $REGISTRY/pharmabot:$GIT_SHA

# Secondary tag: date-based (useful for retention policies)
docker tag pharmabot $REGISTRY/pharmabot:$BUILD_DATE-$GIT_SHA

# Optional: semantic version for named releases
docker tag pharmabot $REGISTRY/pharmabot:v$SEMVER

In your container app configuration, always reference the SHA tag:

YAML
# Azure Container App
image: pharmabotprod.azurecr.io/pharmabot:a3b4c5d

# Not this:
image: pharmabotprod.azurecr.io/pharmabot:latest

Image Scanning

Both ACR and ECR can automatically scan images for known CVEs (Common Vulnerabilities and Exposures) on push.

ACR with Microsoft Defender for Containers:

Bash
# Enable Defender for Containers on the subscription
az security pricing create \
  --name Containers \
  --tier Standard

# This enables automatic vulnerability scanning for all images pushed to ACR.
# Results appear in Microsoft Defender for Cloud dashboard.

Defender for Containers scans for:

  • OS-level CVEs (Debian packages, OpenSSL, etc.)
  • Language package CVEs (Python packages in requirements.txt)
  • Malware signatures
  • Misconfigured Dockerfile settings (running as root, world-writable files)

ECR scan on push:

Bash
# Enable in repository creation (shown above)
# Or enable on an existing repository:
aws ecr put-image-scanning-configuration \
  --repository-name pharmabot \
  --image-scanning-configuration scanOnPush=true

# Retrieve scan results for a specific image
aws ecr describe-image-scan-findings \
  --repository-name pharmabot \
  --image-id imageTag=$GIT_SHA

Blocking deployments on critical CVEs:

In your CI pipeline, fail the build if critical vulnerabilities are found:

Bash
# Check ECR scan results and fail if CRITICAL findings exist
CRITICAL_COUNT=$(aws ecr describe-image-scan-findings \
  --repository-name pharmabot \
  --image-id imageTag=$GIT_SHA \
  --query 'imageScanFindings.findingSeverityCounts.CRITICAL' \
  --output text)

if [ "$CRITICAL_COUNT" != "None" ] && [ "$CRITICAL_COUNT" -gt 0 ]; then
  echo "ERROR: $CRITICAL_COUNT critical vulnerabilities found. Blocking deployment."
  exit 1
fi

Private vs Public Registries

| Feature | Private (ACR/ECR) | Public (Docker Hub) | |---|---|---| | Authentication required | Yes | No (for pulls) | | Cost | Per-storage + per-request | Free tier + paid | | Scanning | Built-in | Paid only | | Network security | VNet integration | Internet only | | Geo-replication | ACR Premium / ECR (multi-region) | No | | Rate limiting | No | Yes (pulls from unauthenticated IPs) |

For production AI services, always use a private registry. Reasons:

  1. Your application code is proprietary. Base images from Docker Hub are fine; your business logic is not public.
  2. Rate limiting. Docker Hub limits unauthenticated pulls to 100/6h per IP. At scale, CI/CD pipelines hit this limit constantly.
  3. Network path. ACR and ECR can be accessed over private endpoints (VNet/VPC). Public registries always route over the internet.

Geo-Replication for Low-Latency Pulls

If you deploy to multiple Azure regions, pulling a 500 MB image from a registry in North Europe for a container in East US adds latency and egress costs. ACR Premium supports geo-replication — the registry automatically replicates images to all configured regions.

Bash
# Enable geo-replication (requires Premium SKU)
az acr replication create \
  --registry pharmabotprod \
  --location eastus

az acr replication create \
  --registry pharmabotprod \
  --location southeastasia

# List replications
az acr replication list --registry pharmabotprod --output table

Once replicated, pharmabotprod.azurecr.io resolves to the nearest replica automatically. Container pulls in East US hit the East US replica, dramatically reducing cold start time.


Complete ACR Workflow: Summary

Here's the full workflow from code change to registry, as a single script:

Bash
#!/bin/bash
set -euo pipefail

# Configuration
REGISTRY_NAME="pharmabotprod"
REGISTRY="$REGISTRY_NAME.azurecr.io"
IMAGE="pharmabot"
GIT_SHA=$(git rev-parse --short HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
BUILD_DATE=$(date -u +%Y%m%dT%H%M%SZ)

echo "Building $IMAGE:$GIT_SHA from branch $GIT_BRANCH"

# 1. Authenticate
az acr login --name $REGISTRY_NAME

# 2. Build with labels for traceability
docker build \
  --label "git.sha=$GIT_SHA" \
  --label "git.branch=$GIT_BRANCH" \
  --label "build.date=$BUILD_DATE" \
  -t $REGISTRY/$IMAGE:$GIT_SHA \
  -t $REGISTRY/$IMAGE:$GIT_BRANCH-latest \
  .

# 3. Push both tags
docker push $REGISTRY/$IMAGE:$GIT_SHA
docker push $REGISTRY/$IMAGE:$GIT_BRANCH-latest

echo "Pushed: $REGISTRY/$IMAGE:$GIT_SHA"
echo "Pushed: $REGISTRY/$IMAGE:$GIT_BRANCH-latest"

# 4. Verify the push
az acr repository show-tags \
  --name $REGISTRY_NAME \
  --repository $IMAGE \
  --orderby time_desc \
  --top 5

This script:

  • Tags with the immutable git SHA (primary) and a branch-mutable tag (secondary)
  • Adds build metadata as image labels (visible in docker inspect)
  • Verifies the push succeeded by listing recent tags
  • Exits on any error (set -euo pipefail)

Summary

Container registries are where your deployment pipeline and your container runtime meet. Key decisions:

  • ACR for Azure-centric workloads; ECR for AWS. Both are enterprise-grade.
  • Never use :latest in production. Tag with git SHA for immutability.
  • Enable scan on push. Both ACR (Defender) and ECR (built-in) scan for CVEs automatically.
  • Managed identities / OIDC authentication in CI/CD instead of long-lived passwords.
  • Geo-replication (ACR Premium) for multi-region deployments to eliminate cross-region pull latency.
  • Lifecycle policies in ECR to prevent unbounded storage growth.

In the next lesson we'll use Docker Compose to wire together the full local development environment — the API, Redis, Postgres, and a mock Azure OpenAI server — so you can develop without hitting real API quotas.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.