Container Registry: Image Storage, Scanning, and Distribution

Set up and secure container registries for storing, scanning, and distributing container images across your CI/CD pipeline and clusters.

published: reading time: 10 min read

Container registries are the artifact stores for your CI/CD pipeline. This guide covers registry options, image tagging strategies, vulnerability scanning, and access control.

When to Use / When Not to Use

When registries make sense

A container registry is essential when you are building and deploying containerized applications. If your CI/CD pipeline produces Docker images, you need somewhere to store them between the build step and the deployment step. Even for small projects, a registry gives you a versioned history of your images that you cannot get from local Docker storage.

Use a registry when you need image promotion between environments. An image built once in the CI pipeline should flow from dev to staging to production without rebuilding. The registry holds the canonical artifact at each stage.

For teams with multiple services, a shared registry lets different pipelines pull base images and intermediate layers without repeatedly downloading from public sources. This speeds up builds and reduces external dependencies.

When to skip or simplify

If your application is not containerized and never will be, a container registry does not add value. Some projects are better served by plain artifact storage (S3, GCS) for JARs, binaries, or Helm charts.

For personal projects or experiments that never leave your local machine, running a local registry or skipping one entirely makes sense. You can always add a registry later when the project grows.

Registry Architecture Flow

flowchart LR
    A[Developer] -->|docker build| B[CI Pipeline]
    B -->|docker push| C[Container Registry]
    C -->|docker pull| D[Staging Cluster]
    C -->|docker pull| E[Production Cluster]
    F[Scanner] -->|periodic scan| C

Registry Options

Cloud-provider registries:

ProviderServiceNotes
AWSECRIntegrated with IAM, VPC endpoints
AzureACRGeo-replication, webhook integration
GCPGCRIntegrated with Cloud Build, IAM
GoogleArtifact RegistrySuccessor to GCR, multi-format

Self-hosted options:

RegistryBest For
HarborEnterprise with authentication, replication
GitLab Container RegistryGitLab-integrated deployments
Docker HubPublic images, simple needs
ChartMuseumHelm charts alongside images

AWS ECR example:

# Create ECR repository
aws ecr create-repository \
  --repository-name myapp/backend \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=AES256

# Login to ECR
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

# Push image
docker build -t myapp/backend:1.0.0 .
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp/backend:1.0.0

Azure ACR example:

# Create ACR
az acr create \
  --resource-group mygroup \
  --name myregistry \
  --sku Standard \
  --location eastus

# Enable admin user
az acr update -n myregistry --admin-enabled true

# Login and push
az acr login --name myregistry
docker build -t myregistry.azurecr.io/myapp:1.0.0 .
docker push myregistry.azurecr.io/myapp:1.0.0

Image Tagging Strategies

Tags identify image versions and control which code deploys where.

Common tagging patterns:

PatternExampleUse Case
Git SHAa1b2c3dPrecise traceability
Semantic version1.2.3Releases
Date-time202603251430CI/CD timestamps
latestlatestDefault, avoid for prod
Environmentprod, stagingEnvironment promotion

Recommended strategy for production:

# Always tag with multiple identifiers
IMAGE="myregistry.azurecr.io/myapp/backend"

# SHA for exact traceability
docker build -t $IMAGE:sha-$(git rev-parse --short HEAD)

# Semantic version from tag
docker build -t $IMAGE:$(cat VERSION)

# Date-time for CI builds
docker build -t $IMAGE:$(date +%Y%m%d%H%M%S)

# Don't use :latest in production manifests
# Instead:
kubectl set image deployment/myapp backend=$IMAGE:sha-a1b2c3d

Immutable tags for releases:

# Helm values for production deployment
image:
  repository: myregistry.azurecr.io/myapp/backend
  tag: "v1.2.3" # Immutable once released
  pullPolicy: IfNotPresent # Only pulls if not present

Vulnerability Scanning

Integrate scanning into your pipeline to catch vulnerabilities before deployment.

Trivy in CI/CD:

# GitHub Actions
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myregistry.azurecr.io/myapp:${{ github.sha }}
    format: "sarif"
    output: "trivy-results.sarif"
    severity: "CRITICAL,HIGH"
    exit-code: "1" # Fail on critical vulnerabilities

- name: Upload Trivy results to GitHub Security tab
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: "trivy-results.sarif"

Harbor scanning:

Harbor integrates Trivy and other scanners natively. Configure in /etc/harbor/harbor.yml:

# Enable CVE prevention
vulnerability:
  severity: # Reject push if severity >= this level
    - high
    - critical
  stop_uploading: true
  stop_downloading: true
  update_bulk: "1h"

Gatekeeper policy:

# OPA Gatekeeper constraint for image signatures
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredImageTag
metadata:
  name: require-tag
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    namespaces: ["production"]
    exemptImages:
      - "myregistry.azurecr.io/base-image:*"

Image Promotion Between Environments

Promote images through environments with validation at each stage.

GitLab CI image promotion:

stages:
  - build
  - scan
  - staging
  - production

build:
  stage: build
  script:
    - docker build -t $IMAGE:build-$CI_COMMIT_SHA .
    - docker push $IMAGE:build-$CI_COMMIT_SHA

scan:
  stage: scan
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE:build-$CI_COMMIT_SHA
  allow_failure: false

promote:staging:
  stage: staging
  script:
    - docker tag $IMAGE:build-$CI_COMMIT_SHA $IMAGE:staging-$CI_COMMIT_SHA
    - docker push $IMAGE:staging-$CI_COMMIT_SHA
    - curl -X POST "https://argo.example.com/api/v1/apps/myapp/sync" \
      -d '{"prune":true,"dryRun":false}'
  only:
    - main

promote:production:
  stage: production
  script:
    - docker tag $IMAGE:build-$CI_COMMIT_SHA $IMAGE:production-$CI_COMMIT_SHA
    - docker push $IMAGE:production-$CI_COMMIT_SHA
    -  # Update deployment manifests or trigger ArgoCD
  when: manual
  only:
    - main

Digest-based promotion for immutability:

# Get image digest
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' myregistry.azurecr.io/myapp:1.0.0)
echo $DIGEST
# Output: myregistry.azurecr.io/myapp@sha256:abc123...

# Deploy by digest (immutable)
docker run myregistry.azurecr.io/myapp@sha256:abc123...

Registry Caching for Faster Builds

Cache images to reduce build times and external dependencies.

GitHub Actions cache for Docker:

- uses: docker/setup-buildx-action@v3
  with:
    driver-opts: |
      image=moby/buildkit:buildx-stable
      network=host

- uses: docker/build-push-action@v5
  with:
    push: true
    tags: myregistry.azurecr.io/myapp:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Registry mirror for air-gapped environments:

# Deploy a pull-through cache
helm install registry-cache stable/docker-registry \
  --set pullThrough.enabled=true \
  --set cache.remoteURL=https://registry-1.docker.io

# Configure Docker daemon on nodes
# /etc/docker/daemon.json
{
  "registry-mirrors": ["https://cache.mycorp.example.com"]
}

Access Control and Authentication

Control who can push, pull, and manage images.

AWS ECR IAM policies:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789:role/deploy-role"
      },
      "Action": [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ]
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789:role/ci-service-account"
      },
      "Action": ["ecr:*"]
    }
  ]
}

Azure RBAC for ACR:

# Reader role for production pods
az role assignment create \
  --assignee-object-id $(kubectl get serviceaccount default -n production -o jsonpath='{.metadata.uid}') \
  --role AcrPull \
  --scope /subscriptions/.../resourceGroups/mygroup/providers/Microsoft.ContainerRegistry/registries/myregistry

# Contributor for CI/CD pipelines
az role assignment create \
  --assignee $CI_SERVICE_PRINCIPAL_ID \
  --role Contributor \
  --scope /subscriptions/.../resourceGroups/mygroup/providers/Microsoft.ContainerRegistry/registries/myregistry

Kubernetes image pull secrets:

# Create image pull secret
kubectl create secret docker-registry acr-secret \
  --docker-server=myregistry.azurecr.io \
  --docker-username=myuser \
  --docker-password=$(az keyvault secret show --name acr-password --vault-name myvault -o tsv --query value) \
  --docker-email=myuser@example.com

# Reference in service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: production
imagePullSecrets:
  - name: acr-secret

Production Failure Scenarios

Common Registry Failures

FailureImpactMitigation
Registry authentication token expiredCannot pull or push images, deployments failUse service accounts with rotating credentials
Image not found after tag deletionDeployment tries to pull deleted imageNever delete tags in production, use immutable tags
Quota exceeded on cloud registryCannot push new imagesMonitor storage usage, set lifecycle policies
Network partition to registryBuilds cannot push, deployments cannot pullUse multi-region replication, local registry cache
Vulnerability scan blocking deploysCritical CVE stops production deploymentDefine triage process for CVEs, do not scan blindly

Image Pull Failures

flowchart TD
    A[Pod Schedule] --> B{Pull Image}
    B -->|Success| C[Pod Starts]
    B -->|Fail| D{Error Type}
    D -->|Image Not Found| E[Check tag exists in registry]
    D -->|Auth Failed| F[Refresh pull secret]
    D -->|Quota Exceeded| G[Delete old images]
    E --> H[Redeploy]
    F --> H
    G --> H

Observability Hooks

What to monitor:

  • Image push success/failure rate
  • Pull request latency by region
  • Storage consumption growth rate
  • Vulnerability scan results trend
  • Authentication failure spikes
# AWS ECR - check repository size
aws ecr describe-repositories --query 'repositories[].{name:repositoryName,size:imageScanningConfiguration.scanOnPush}'

# Azure ACR - check usage
az acr show-usage --name myregistry

# Docker Hub - check rate limits
curl -s -o /dev/null -w "%{http_code}" https://hub.docker.com/v2/

Common Pitfalls / Anti-Patterns

Using the :latest tag in production manifests

When your manifest references myapp:latest, you have no idea which image actually deployed. If you need to roll back, you cannot because :latest keeps changing. Always use specific immutable tags (commit SHA, semantic version, or date-based).

Not scanning images before deployment

Pushing unscanned images to production means you discover vulnerabilities after they are running. Integrate Trivy or similar scanners into your CI pipeline and block pushes when critical CVEs are found.

Leaving unused images in the registry

Old images accumulate and consume storage. Without lifecycle policies, your registry bill grows while nobody knows which images are actually in use. Set up retention policies that delete images not referenced by any deployment.

Mixing image sources in one pipeline

Using Docker Hub for base images, your private registry for app images, and a third-party registry for dependencies makes it hard to track where each piece comes from. Keep a clear lineage from build to registry to deployment.

Forgetting about cross-region replication

If your production cluster is in us-west-2 but your registry is only in us-east-1, every image pull crosses the country. Configure replication for the regions where your clusters run.

Quick Recap

Key Takeaways

  • Use specific immutable tags (SHA, semver) instead of :latest in production
  • Integrate vulnerability scanning into CI to catch CVEs before deployment
  • Configure access control through cloud IAM, not shared passwords
  • Set up registry replication for multi-region deployments
  • Monitor push/pull success rates, storage growth, and scan results

Registry Health Checklist

# Verify you can pull your production image
docker pull myregistry.azurecr.io/myapp:prod && echo "Pull successful"

# Check for critical CVEs before deploying
trivy image --severity CRITICAL,HIGH myregistry.azurecr.io/myapp:v1.2.3

# List unused images (older than 90 days)
aws ecr list-images --repository-name myapp --filter '{"tagStatus": "UNTAGGED"}'

# Verify pull secret works in Kubernetes
kubectl get secret acr-secret --output jsonpath='{.data.\.dockerconfigjson}' | base64 -d

Trade-off Summary

RegistryHosted vs Self-HostedAuthCostBest For
Docker HubBothDocker AuthFree tier / paidOpen source images
ECRCloud-hostedIAMPay per storage + egressAWS workloads
GCR / Artifact RegistryCloud-hostedIAM + VMDFPay per storage + egressGCP workloads
ACRCloud-hostedIAM + admin keysPay per storage + egressAzure workloads
HarborSelf-hostedOIDC, LDAP, localInfrastructure costEnterprise / air-gapped
GitHub PackagesCloud-hostedGitHub tokenStorage + bandwidthGitHub-native workflows
GitLab Container RegistryCloud-hostedGitLab tokenStorage + bandwidthGitLab-native workflows

Conclusion

Container registries are critical infrastructure for secure, fast deployments. Use semantic or commit-based tagging for traceability, integrate vulnerability scanning into your pipeline, and enforce access control through cloud IAM or Kubernetes secrets. For more on CI/CD patterns, see our CI/CD Pipelines overview, and for deployment strategies, see our Deployment Strategies guide.

Category

Related Posts

Container Images: Building, Optimizing, and Distributing

Learn how Docker container images work, layer caching strategies, image optimization techniques, and how to publish your own images to container registries.

#docker #containers #devops

Docker Networking: From Bridge to Overlay

Master Docker's networking models—bridge, host, overlay, and macvlan—for connecting containers across hosts and distributed applications.

#docker #networking #containers

Docker Fundamentals

Learn Docker containerization fundamentals: images, containers, volumes, networking, and best practices for building and deploying applications.

#docker #containers #devops