Part 7 of the Docker Roadmap Series

Congratulations! You’ve built your first lean, secure Docker image. It’s a thing of beauty, minimal, purposeful and probably under 50MB. You’re rightfully proud of your digital craftsmanship.

But now what? Are you going to email it to your teammates as a ZIP file? Upload it to Google Drive? Carrier pigeon?

Welcome to the world of container registries, the sophisticated distribution network that makes Docker actually useful in the real world. Think of registries as the App Store for your containers, except instead of overpriced games and productivity apps nobody uses, you’re distributing the building blocks of modern software infrastructure.

What is a Container Registry (And Why You Need One)

A container registry is essentially a centralized repository where you store, manage, and distribute your Docker images. It’s like GitHub, but instead of storing source code, you’re storing ready-to-run application packages.

Here’s the thing: Docker images are just fancy tar files with some metadata sprinkled on top. You could manually copy these files around, but that’s like delivering mail by walking door-to-door instead of using the postal system. Registries provide:

  • Centralized storage: One place to store all your images
  • Version management: Multiple tags per image, easy rollbacks
  • Access control: Who can push/pull what images
  • Global distribution: CDN-like distribution for faster pulls
  • Security scanning: Automated vulnerability detection
  • Build integration: CI/CD pipelines can push/pull automatically

Imagine a team of five developers working on the same microservice. Without a centralized registry, each developer might have slightly different versions of the base image, or worse, struggle to get the exact same application image running locally as what’s in production. This quickly leads to the dreaded “it works on my machine!” syndrome, wasted hours debugging environment inconsistencies, and a slow, error-prone deployment process. A container registry eradicates this chaos.

Docker Hub: The Neighborhood Everyone Knows

Docker Hub is the default registry that comes with Docker. It’s where you’ve likely already pulled images from, such as that node:latest image. When you pull an image like node:latest (or ubuntu:latest, nginx:stable, etc.) without a username prefix, you’re interacting with Official Images. These are curated, maintained, and supported by Docker and the upstream project communities (like Node.js or Ubuntu). Think of them as the verified, high-quality, frequently updated building blocks.

Docker Hub is like the Walmart of container registries: convenient, widely used, but not always the classiest option.

Your First Push to Docker Hub

Now, let’s get your beautiful custom image up on Docker Hub so the world can admire your work. Unlike the Official Images we just discussed, images you push will be associated with your Docker Hub account.

# First, you need to tag your image with your Docker Hub username.
# Notice the "yourusername/" prefix – this tells Docker it's YOUR repository, not an Official Image.
docker tag my-lean-node-app yourusername/my-lean-node-app:1.0.0
 
# Login to Docker Hub (you'll need an account)
docker login
# Enter your username and password when prompted
 
# Push your image
docker push yourusername/my-lean-node-app:1.0.0
 
# Push multiple tags at once
docker tag my-lean-node-app yourusername/my-lean-node-app:latest
docker push yourusername/my-lean-node-app:latest

Important: The image name format for Docker Hub is username/repository:tag. If you don’t include your username, Docker assumes you’re trying to push to the official library (spoiler: you don’t have permission).

Pulling from Docker Hub

Now anyone (including your future self on a different machine) can pull your image:

# Pull your specific version
docker pull yourusername/my-lean-node-app:1.0.0
 
# Pull the latest version
docker pull yourusername/my-lean-node-app:latest
 
# Run it directly without explicitly pulling first
docker run -p 3000:3000 yourusername/my-lean-node-app:1.0.0

Docker Hub Limitations (The Fine Print)

Docker Hub is free for public repositories, but it comes with some… quirks:

Rate Limiting: Docker Hub limits anonymous pulls to 100 per 6 hours per IP address. For authenticated users, it’s 200 pulls per 6 hours. Hit that limit and your CI/CD pipeline starts failing with cryptic error messages.

# You'll see something like this when you hit the limit:
# ERROR: toomanyrequests: You have reached your pull rate limit

Public by Default: Unless you pay for Docker Hub Pro, your images are public. That means your proprietary application code is visible to anyone who knows where to look.

Limited Storage: Free accounts get one private repository. That’s… not a lot.

No Enterprise Features: Want vulnerability scanning, automated builds, or team management? That’ll cost extra.

For serious production use, Docker Hub is like using a bicycle on the highway, it technically works, but you’re going to want something more robust.

Private Registries: Where Adults Store Their Images

When you’re ready to graduate from the kiddie pool, private registries offer the control, security, and features that production environments demand.

AWS Elastic Container Registry (ECR)

ECR is Amazon’s container registry service, and if you’re already in the AWS ecosystem, it’s a no-brainer.

Setting up ECR:

# Install AWS CLI if you haven't already
# Configure your AWS credentials first: aws configure
 
# Create a repository
aws ecr create-repository --repository-name my-app --region us-west-2
 
# Get login token and authenticate Docker
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com
 
# Tag your image for ECR
docker tag my-lean-node-app:latest 123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app:latest
 
# Push to ECR
docker push 123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app:latest

ECR Benefits:

  • Integrates seamlessly with ECS, EKS, and other AWS services
  • IAM-based access control (finally, real security!)
  • Automatic vulnerability scanning
  • Lifecycle policies to automatically clean up old images
  • No rate limiting for pulls within AWS

ECR Pricing: You pay for storage (~$0.10/GB/month) and data transfer. For most applications, this is pennies compared to the value it provides.

Google Container Registry (GCR) / Artifact Registry

Google offers two registry services: the older GCR and the newer Artifact Registry. Use Artifact Registry for new projects.

# Configure gcloud CLI
gcloud auth configure-docker
 
# Tag for GCR
docker tag my-lean-node-app:latest gcr.io/your-project-id/my-app:latest
 
# Push to GCR
docker push gcr.io/your-project-id/my-app:latest
 
# For Artifact Registry (recommended)
docker tag my-lean-node-app:latest us-central1-docker.pkg.dev/your-project-id/my-repo/my-app:latest
docker push us-central1-docker.pkg.dev/your-project-id/my-repo/my-app:latest

GitHub Container Registry (GHCR)

If your code lives on GitHub, GHCR is a natural choice. It’s tightly integrated with GitHub Actions and supports both public and private repositories.

# Login with a personal access token
echo $GITHUB_TOKEN | docker login ghcr.io -u yourusername --password-stdin
 
# Tag for GHCR
docker tag my-lean-node-app:latest ghcr.io/yourusername/my-app:latest
 
# Push to GHCR
docker push ghcr.io/yourusername/my-app:latest

Azure Container Registry (ACR)

Microsoft’s offering, well-integrated with Azure services:

# Login to ACR
az acr login --name myregistry
 
# Tag and push
docker tag my-lean-node-app:latest myregistry.azurecr.io/my-app:latest
docker push myregistry.azurecr.io/my-app:latest

Self-Hosted Registries: For the Control Freaks

Sometimes, an organization needs complete, granular control over its registry infrastructure. While cloud providers offer excellent services, there are specific scenarios where self-hosting makes strategic sense. Beyond strict compliance requirements or operating in air-gapped environments (where internet access is restricted for security), you might opt for a self-hosted solution for:

  • Cost Control for Extremely High Pull Volumes: For organizations with massive internal traffic and frequent image pulls across many services, the cumulative data transfer and storage costs of cloud registries can become substantial. Self-hosting can offer more predictable and potentially lower costs at extreme scale.
  • Avoiding Vendor Lock-in: Relying solely on a single cloud provider’s registry tightly integrates your build and deployment pipelines with their ecosystem. A self-hosted or hybrid approach offers greater flexibility if you need to operate across multiple cloud providers or migrate in the future.
  • Deep Customization and Integration: If you have highly specific needs for authentication, authorization, or integration with existing internal systems that cloud offerings don’t fully support, a self-hosted solution provides the flexibility to customize to your heart’s content.
  • Trust Issues with Cloud Providers: While rare, some organizations prefer to keep all their critical intellectual property entirely within their own data centers due to specific security policies or risk assessments.

If any of these resonate, then running your own registry might be the path for you.

Running Your Own Registry

Docker provides an official registry image that you can run anywhere:

# Run a simple registry on port 5000
docker run -d -p 5000:5000 --name my-registry registry:2
 
# Tag an image for your local registry
docker tag my-lean-node-app:latest localhost:5000/my-app:latest
 
# Push to your local registry
docker push localhost:5000/my-app:latest
 
# Pull from your local registry
docker pull localhost:5000/my-app:latest

Production Registry Setup:

For production use, you’ll want persistence, TLS, and authentication:

# docker-compose.yml for a production registry
services:
  registry:
    image: registry:2
    ports:
      - "443:5000"
    environment:
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/registry.crt
      REGISTRY_HTTP_TLS_KEY: /certs/registry.key
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
    volumes:
      - ./data:/var/lib/registry
      - ./certs:/certs
      - ./auth:/auth
    restart: unless-stopped

Harbor: The Enterprise Registry

If you want a self-hosted registry with all the bells and whistles, Harbor is your best bet. It’s like running your own private Docker Hub with enterprise features:

  • Web UI for image management
  • Role-based access control
  • Vulnerability scanning
  • Image signing and trust
  • Replication between registries
  • Helm chart repository

Harbor is overkill for small projects, but if you’re managing containers at scale, it’s worth the setup complexity.

Image Tagging Strategies: Version Control for Containers

Tagging is where most teams mess up their container strategy. Bad tagging is like having a filing system where everything is named “document.txt” - technically it works, but good luck finding anything later.

The Anatomy of a Tag

A complete Docker image reference looks like this:

[registry]/[namespace]/[repository]:[tag]

Examples:

  • docker.io/library/node:18-alpine (Docker Hub official image)
  • ghcr.io/microsoft/dotnet:6.0 (GitHub Container Registry)
  • 123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app:v1.2.3 (AWS ECR)

Semantic Versioning: The Adult Approach

Use semantic versioning (semver) for your application images:

# Build and tag with multiple semantic versions
docker build -t my-app:1.2.3 .
docker tag my-app:1.2.3 my-app:1.2
docker tag my-app:1.2.3 my-app:1
docker tag my-app:1.2.3 my-app:latest
 
# Push all tags
docker push my-app:1.2.3
docker push my-app:1.2
docker push my-app:1
docker push my-app:latest

This gives consumers flexibility:

  • my-app:1.2.3 - Exact version (immutable, safe for production)
  • my-app:1.2 - Latest patch version (gets security fixes)
  • my-app:1 - Latest minor version (gets new features)
  • my-app:latest - Bleeding edge (use with caution)

Environment-Based Tagging

For applications that vary by environment:

# Tag with environment suffixes
docker tag my-app:1.2.3 my-app:1.2.3-dev
docker tag my-app:1.2.3 my-app:1.2.3-staging
docker tag my-app:1.2.3 my-app:1.2.3-prod
 
# Or use environment-specific repositories
docker tag my-app:1.2.3 my-registry/my-app-dev:1.2.3
docker tag my-app:1.2.3 my-registry/my-app-prod:1.2.3

Git-Based Tagging

For development builds, use Git commit hashes:

# Tag with Git commit hash
COMMIT_HASH=$(git rev-parse --short HEAD)
docker build -t my-app:$COMMIT_HASH .
docker tag my-app:$COMMIT_HASH my-app:dev
 
# Tag with branch name (sanitized)
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD | sed 's/[^a-zA-Z0-9]/-/g')
docker tag my-app:$COMMIT_HASH my-app:$BRANCH_NAME

The “latest” Tag Controversy

Here’s a controversial opinion that comes from years of experience: stop using the latest tag in production. Seriously.

The latest tag is like naming your save files “final_document.doc”, “final_document_v2.doc”, “final_document_ACTUALLY_FINAL.doc”. It’s convenient until you need to know exactly what you’re running, or worse, need to roll back. I’ve personally seen production deployments go sideways at 2 AM because a latest tag unexpectedly contained a breaking API change that wasn’t properly tested. That kind of pain teaches you quickly.

# This is a recipe for confusion
docker run my-app:latest
 
# This is explicit and trackable
docker run my-app:1.2.3

Use latest for local development and testing, but pin specific versions in production configurations.

Security and Image Scanning

Storing images in registries isn’t just about convenience, it’s about security. Modern registries provide vulnerability scanning that can catch security issues before they reach production. These powerful tools analyze your image layers for known vulnerabilities (CVEs), outdated dependencies, and potential misconfigurations, essentially providing a “bill of materials” for your image’s contents. This insight is crucial for maintaining a robust and secure software supply chain.

Registry-Native Scanning

Most cloud registries provide built-in vulnerability scanning:

AWS ECR:

# Enable scanning on push
aws ecr put-image-scanning-configuration \
    --repository-name my-app \
    --image-scanning-configuration scanOnPush=true
 
# Manual scan
aws ecr start-image-scan \
    --repository-name my-app \
    --image-id imageTag=latest
 
# Get scan results
aws ecr describe-image-scan-findings \
    --repository-name my-app \
    --image-id imageTag=latest

Google Artifact Registry:

# Scanning is enabled by default, view results
gcloud artifacts docker images scan IMAGE_URL

Third-Party Scanning Tools

For more advanced scanning, consider dedicated tools:

Trivy (Open source, fast):

# Install trivy
# Scan an image
trivy image my-app:latest
 
# Scan and fail on high/critical vulnerabilities
trivy image --exit-code 1 --severity HIGH,CRITICAL my-app:latest

Snyk (Commercial, great CI/CD integration):

# Scan with Snyk
snyk container test my-app:latest
 
# Monitor for new vulnerabilities
snyk container monitor my-app:latest

Image Signing and Trust

For high-security environments or when you need an extra layer of assurance, image signing is crucial. This process cryptographically signs your images, ensuring the image you pull is exactly the one the publisher intended, without any tampering or unauthorized modifications along the way. While this can add complexity, it’s a vital practice for critical production systems.

Docker Content Trust:

# Enable content trust
export DOCKER_CONTENT_TRUST=1
 
# Push a signed image
docker push my-registry/my-app:1.2.3
# This will prompt for signing keys

Cosign (Modern, ECDSA-based signing):

# Generate keys
cosign generate-key-pair
 
# Sign an image
cosign sign --key cosign.key my-registry/my-app:1.2.3
 
# Verify signature
cosign verify --key cosign.pub my-registry/my-app:1.2.3

Registry Best Practices: The Professional Playbook

1. Use Immutable Tags for Production

Never overwrite tags used in production. If you need to update an image, create a new tag:

# Bad: Overwriting production tags
docker tag my-app:new-features my-app:prod
docker push my-app:prod  # Overwrites existing prod tag!
 
# Good: Create new versioned tags
docker tag my-app:new-features my-app:1.3.0
docker push my-app:1.3.0
# Update your deployment to use 1.3.0

2. Implement Lifecycle Policies

Don’t let old images pile up like digital hoarding:

ECR Lifecycle Policy Example:

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Keep last 10 images",
      "selection": {
        "tagStatus": "any",
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}

3. Use Multi-Architecture Builds

Modern applications should support multiple architectures (AMD64, ARM64):

# Build for multiple architectures
docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t my-registry/my-app:1.2.3 \
    --push .

4. Secure Your Registry Access

Use proper authentication and authorization:

# Use service accounts for CI/CD, not personal credentials
# AWS ECR with IAM roles
aws ecr get-login-password --region us-west-2 | \
    docker login --username AWS --password-stdin \
    123456789012.dkr.ecr.us-west-2.amazonaws.com
 
# GitHub Actions with GITHUB_TOKEN
echo $GITHUB_TOKEN | docker login ghcr.io -u ${{ github.actor }} --password-stdin

5. Monitor Registry Usage

Keep track of image pulls, storage usage, and costs:

# Check ECR repository sizes
aws ecr describe-repositories --query 'repositories[*].[repositoryName,repositorySizeInBytes]' --output table
 
# Monitor Docker Hub rate limits
curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | \
    jq -r .token | \
    curl -H "Authorization: Bearer $(cat -)" \
    https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest -I 2>&1 | \
    grep -i ratelimit

CI/CD Integration: Automating the Pipeline

Registries really shine when integrated with CI/CD pipelines. Here’s how to automate image building and pushing:

GitHub Actions Example

# .github/workflows/build-and-push.yml
name: Build and Push Docker Image
 
on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
 
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
 
    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
 
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
 
    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}

GitLab CI Example

# .gitlab-ci.yml
stages:
  - build
  - push
 
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
 
build-image:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - |
      if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
        docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
        docker push $CI_REGISTRY_IMAGE:latest
      fi

Common Registry Gotchas (Learn From Others’ Pain)

The “It Worked On My Machine” Registry Edition

Problem: Your image works locally but fails when pulled from the registry.

Cause: Platform mismatches. Your M1 Mac builds ARM64 images, but your production server expects AMD64.

Solution: Use multi-platform builds or specify the platform explicitly:

# Build for specific platform
docker build --platform linux/amd64 -t my-app:latest .
 
# Or use buildx for multi-platform
docker buildx build --platform linux/amd64,linux/arm64 -t my-app:latest --push .

The Registry That Ate Your Wallet

Problem: Unexpected cloud registry costs.

Cause: Not implementing lifecycle policies, storing massive debug images, or excessive cross-region data transfer.

Solution: Set up lifecycle policies, use .dockerignore properly, and consider registry location:

# Check your image sizes regularly
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h
 
# Implement aggressive cleanup for development images
docker image prune -a --filter "until=24h"

The Authentication Nightmare

Problem: Authentication randomly fails in CI/CD.

Cause: Expired tokens, incorrect scopes, or rate limiting.

Solution: Use long-lived service accounts and implement proper retry logic:

# GitHub Actions: Use GITHUB_TOKEN, not personal access tokens
# AWS: Use IAM roles, not access keys
# Docker Hub: Use access tokens, not passwords

The Manifest Not Found Mystery

Problem: docker pull fails with “manifest not found” even though the image exists.

Cause: Usually a platform mismatch or corrupted push.

Solution: Check the manifest and re-push if necessary:

# Check what platforms are available
docker manifest inspect my-registry/my-app:latest
 
# Force re-push if corrupted
docker push my-registry/my-app:latest --force

Registry Comparison: Choosing Your Poison

When deciding where to store your precious container images, a few key factors come into play beyond just features. Here’s an updated comparison to help you weigh your options:

FeatureDocker HubAWS ECRGoogle ARGitHub CRSelf-Hosted
Free Tier1 private repo500MB/month0.5GB/monthFree for publicHosting costs
Rate Limits100-200/6hrsNone (within AWS)None (within GCP)NoneNone
Vulnerability ScanningPro plan onlyBuilt-inBuilt-inBuilt-inDIY
Global CDNYesRegionalGlobalGlobalDIY
Enterprise FeaturesPaid plansFull AWS integrationFull GCP integrationGitHub integrationFull control
Ease of SetupInstantMediumMediumEasyComplex

Recommendation:

  • Hobbyist/Open Source: GitHub Container Registry
  • AWS Shop: ECR (no contest)
  • Google Cloud: Artifact Registry
  • Multi-Cloud/Enterprise: Harbor or cloud-native hybrid
  • Security Obsessed: Self-hosted with Harbor

The Bottom Line

Container registries are the circulatory system of your containerized infrastructure. They move your images from development to production, handle versioning, provide security scanning, and integrate with your CI/CD pipelines.

Stop treating registries as an afterthought. Choose one that fits your security, compliance, and operational requirements. Implement proper tagging strategies from day one. Set up lifecycle policies before your storage costs spiral out of control. And for the love of all that’s holy, stop using latest tags in production.

Your registry strategy is as important as your image building strategy. Get it right, and deployments become smooth and predictable. Get it wrong, and you’ll be debugging authentication issues at 2 AM while your production deployment hangs in