Loading content...
Container images are only useful if they can be stored, shared, and deployed reliably. Container registries serve as the central distribution infrastructure for containerized software—they're to containers what package managers are to libraries, but with critical differences in scale, security, and operational requirements.
Every time you run docker pull nginx, you're interacting with a registry. Every CI/CD pipeline that builds and deploys containers relies on registries. Every Kubernetes cluster fetches images from registries. Understanding registries deeply is essential for operating containerized infrastructure at any scale.
By the end of this page, you will understand how container registries work internally, the differences between public and private registries, authentication and authorization mechanisms, replication and caching strategies, registry security best practices, and how to choose and operate registries at scale.
A container registry is a stateless, scalable server-side application that stores and distributes container images. At its core, a registry is a specialized content-addressable storage system with an HTTP API for uploading (pushing) and downloading (pulling) image layers and manifests.
Core Registry Components:
| Component | Function | Details |
|---|---|---|
| API Frontend | HTTP endpoints for push/pull | Implements OCI Distribution Spec |
| Authentication | Identity verification | Token-based, OAuth, LDAP, etc. |
| Authorization | Access control | Repository-level permissions |
| Storage Backend | Blob and manifest storage | Filesystem, S3, GCS, Azure Blob |
| Garbage Collection | Cleanup unused layers | Removes unreferenced blobs |
| Notifications | Webhooks on events | Push notifications for CI/CD integration |
1234567891011121314151617181920212223242526272829303132
Container Registry Architecture┌────────────────────────────────────────────────────────────────────────┐│ REGISTRY ││ ┌────────────────────────────────────────────────────────────────┐ ││ │ API Layer │ ││ │ /v2/ (OCI Distribution Specification) │ ││ │ /v2/<name>/manifests/<reference> - Get/Put image manifests │ ││ │ /v2/<name>/blobs/<digest> - Get/Put layer blobs │ ││ │ /v2/<name>/tags/list - List image tags │ ││ └─────────────────────────┬──────────────────────────────────────┘ ││ │ ││ ┌─────────────────────────▼──────────────────────────────────────┐ ││ │ Authentication / Authorization │ ││ │ Token Service ∙ OAuth ∙ LDAP/AD ∙ RBAC ∙ Repository Policies │ ││ └─────────────────────────┬──────────────────────────────────────┘ ││ │ ││ ┌─────────────────────────▼──────────────────────────────────────┐ ││ │ Storage Backend │ ││ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ ││ │ │ Manifests │ │ Blobs │ │ Tag Index │ │ ││ │ │ (JSON) │ │ (Layers) │ │ (Mapping) │ │ ││ │ └─────────────┘ └─────────────┘ └─────────────┘ │ ││ │ │ ││ │ Backends: Local FS │ AWS S3 │ Google GCS │ Azure Blob │ ││ └────────────────────────────────────────────────────────────────┘ │└────────────────────────────────────────────────────────────────────────┘ ▲ ▲ docker push docker pull │ │ ┌─────────┴─────────┐ ┌──────────┴──────────┐ │ Developer/CI │ │ Kubernetes / Docker │ └───────────────────┘ └─────────────────────┘The OCI Distribution Specification:
Container registries implement the OCI Distribution Specification, ensuring interoperability across different registry implementations. This means images pushed to Docker Hub can be pulled by any OCI-compliant client, and vice versa.
Key API operations:
| Operation | Endpoint | Purpose |
|---|---|---|
| Check support | GET /v2/ | Verify registry supports v2 API |
| List tags | GET /v2/<name>/tags/list | Get all tags for a repository |
| Get manifest | GET /v2/<name>/manifests/<ref> | Download image manifest |
| Check blob | HEAD /v2/<name>/blobs/<digest> | Check if layer exists |
| Get blob | GET /v2/<name>/blobs/<digest> | Download layer content |
| Push blob | POST + PUT /v2/<name>/blobs/ | Upload layer content |
| Push manifest | PUT /v2/<name>/manifests/<ref> | Upload image manifest |
Registries store layers by their SHA256 digest. If 1000 images share the same base layer, that layer is stored exactly once. This deduplication happens automatically at the registry level, saving enormous amounts of storage and bandwidth.
Organizations typically use a combination of public and private registries, each serving different purposes in the software supply chain.
Public Registries:
Public registries host open-source and vendor-provided images accessible to everyone:
| Registry | URL | Notable Features |
|---|---|---|
| Docker Hub | hub.docker.com | Largest registry, official images, 100 free pulls/6hr for anonymous |
| GitHub Container Registry | ghcr.io | GitHub integration, GitHub Actions native, free for public repos |
| Quay.io | quay.io | Red Hat managed, security scanning, public and private |
| Google Container Registry | gcr.io / mirror.gcr.io | Google official images, Kubernetes components |
| Amazon ECR Public | public.ecr.aws | AWS official images, no pull limits |
| Microsoft Container Registry | mcr.microsoft.com | Microsoft official images, .NET, SQL Server |
Private Registries:
Private registries store proprietary images with access control. Every major cloud provider offers managed registry services:
| Service | Provider | Key Features |
|---|---|---|
| Amazon ECR | AWS | IAM integration, image scanning, replication, lifecycle policies |
| Google Artifact Registry | GCP | Multi-format (Docker, npm, Maven), Container Analysis, GKE integration |
| Azure Container Registry | Azure | Geo-replication, ACR Tasks, Helm charts, OCI artifacts |
| Docker Hub Private | Docker | Simple setup, vulnerability scanning, team management |
| GitHub Container Registry | GitHub | GITHUB_TOKEN auth, Actions integration, Packages API |
| GitLab Container Registry | GitLab | Built into GitLab CI, seamless pipelines |
Self-Hosted Registry Options:
For maximum control, organizations can run their own registries:
Most organizations use cloud-managed registries for simplicity—ECR in AWS environments, Artifact Registry in GCP, ACR in Azure. Self-hosted registries (especially Harbor) make sense for multi-cloud environments, air-gapped networks, or when you need advanced features like P2P distribution at scale.
Registry security centers on two questions: Who is accessing the registry? (authentication) and What are they allowed to do? (authorization).
Authentication Mechanisms:
| Method | How It Works | Use Case |
|---|---|---|
| Basic Auth | Username/password in HTTP header | Simple setups, dev environments |
| Bearer Tokens | Short-lived JWT tokens from auth server | Production environments |
| OAuth2 / OIDC | Federated identity (Google, Azure AD) | Enterprise SSO integration |
| Cloud IAM | Native cloud identity (AWS IAM, GCP SA) | Kubernetes in cloud environments |
| mTLS | Client certificate authentication | High-security environments |
1234567891011121314151617181920212223
Docker Login Flow:================== 1. docker login myregistry.com └─→ docker: "Give me credentials for myregistry.com" └─→ User: enters username/password 2. docker pull myregistry.com/myapp:1.0 └─→ docker → registry: GET /v2/ (anonymous) └─→ registry: 401 Unauthorized WWW-Authenticate: Bearer realm="auth.myregistry.com/token" └─→ docker → auth server: GET /token?service=myregistry&scope=repository:myapp:pull (with Basic Auth: base64(username:password)) └─→ auth server: 200 OK { "token": "eyJhbG..." } └─→ docker → registry: GET /v2/myapp/manifests/1.0 Authorization: Bearer eyJhbG... └─→ registry: 200 OK (manifest content) └─→ docker → registry: GET /v2/myapp/blobs/sha256:abc... Authorization: Bearer eyJhbG... └─→ registry: 200 OK (layer content)Authorization Patterns:
Once authenticated, registries control what users can do through authorization policies:
1234567891011121314151617181920212223
# Create a secret with registry credentialskubectl create secret docker-registry regcred \ --docker-server=myregistry.com \ --docker-username=myuser \ --docker-password=mypassword \ --docker-email=user@example.com ---# Reference the secret in Pod specapiVersion: v1kind: Podmetadata: name: my-appspec: containers: - name: app image: myregistry.com/myapp:1.0 imagePullSecrets: - name: regcred ---# For cluster-wide access, patch the default ServiceAccountkubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "regcred"}]}'Registry credentials are high-value targets. Compromised credentials can lead to supply chain attacks via malicious image injection. Use workload identity (IRSA in AWS, Workload Identity in GCP) instead of static credentials where possible. Rotate credentials regularly and monitor for unusual access patterns.
Operating registries at scale introduces challenges around performance, storage management, availability, and cost. Understanding these operational aspects is crucial for platform engineers.
Storage and Lifecycle Management:
Without active management, registries accumulate data indefinitely. Image layers from long-deleted tags remain because they share storage with other images.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
{ "rules": [ { "rulePriority": 1, "description": "Keep only the last 10 production images", "selection": { "tagStatus": "tagged", "tagPrefixList": ["prod-"], "countType": "imageCountMoreThan", "countNumber": 10 }, "action": { "type": "expire" } }, { "rulePriority": 2, "description": "Delete untagged images older than 1 day", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 1 }, "action": { "type": "expire" } }, { "rulePriority": 3, "description": "Delete dev images older than 14 days", "selection": { "tagStatus": "tagged", "tagPrefixList": ["dev-", "feature-"], "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 14 }, "action": { "type": "expire" } } ]}Garbage Collection:
Even after deleting images, the underlying layer blobs may remain referenced by other images. Registries require explicit garbage collection to reclaim storage:
123456789101112
# Docker Distribution registry garbage collection# First, identify unreferenced blobs (dry run)registry garbage-collect --dry-run /etc/docker/registry/config.yml # Then actually clean them upregistry garbage-collect --delete-untagged /etc/docker/registry/config.yml # Harbor garbage collection (via API or UI)# Schedule via System Administration > Configuration > Garbage Collection # For ECR, lifecycle policies handle this automatically# For GCR/Artifact Registry, enable automatic cleanup policiesDocker Hub limits free anonymous pulls to 100 per 6 hours per IP, and free authenticated users to 200. This can cause cascading failures when many nodes pull simultaneously. Solutions include: authenticating to Docker Hub, using Docker Hub mirror, implementing a pull-through cache (Harbor, Distribution), or using alternative registries for public images.
For production systems, registry availability is critical—if the registry is down, new deployments fail and autoscaling breaks. Replication ensures both availability and performance for geographically distributed systems.
Replication Patterns:
| Pattern | Description | Use Case |
|---|---|---|
| Active-Active Multi-Region | Images pushed to any region replicate to all | Global deployments needing fast local pulls |
| Primary-Secondary | Images pushed to primary, replicate to secondaries | Single-region development, multi-region production |
| Pull-Through Cache | Edge registries cache images from upstream | Edge locations, rate limit mitigation |
| Cross-Cloud | Replicate between cloud providers | Multi-cloud strategies, disaster recovery |
123456789101112131415161718192021
# Create Premium tier ACR with geo-replicationaz acr create \ --name mycompanyregistry \ --resource-group mygroup \ --sku Premium \ --location eastus # Add replica regionsaz acr replication create \ --registry mycompanyregistry \ --location westeurope az acr replication create \ --registry mycompanyregistry \ --location southeastasia # List replicationsaz acr replication list --registry mycompanyregistry # Clients in each region pull from geographically closest replica# Push to any region, and replication handles distributionHarbor Replication Configuration:
123456789101112131415161718192021222324
# Harbor replication rule (via API or UI)# Automatically push new images to remote registries replication_policy: name: "prod-to-dr" src_registry: type: harbor url: https://harbor.primary.example.com dest_registry: type: harbor # or docker-hub, aws-ecr, google-gcr, azure-acr url: https://harbor.dr.example.com credential: type: basic access_key: "replication-user" access_secret: "***" trigger: type: push # Replicate on push (vs scheduled) filters: - type: name value: "production/**" # Only replicate production repos - type: tag value: "v*" # Only replicate version tags deletion: true # Replicate deletions override: true # Overwrite existing at destinationFor self-hosted registries, HA typically involves: multiple registry instances behind a load balancer, shared storage backend (S3/GCS for blobs, PostgreSQL/Redis for metadata), health checks, and automatic failover. Managed registries (ECR, GCR, ACR) handle this automatically with their SLAs.
The container registry is a critical point in the software supply chain. A compromised registry can lead to supply chain attacks affecting every system that pulls from it. Security must be defense-in-depth.
Security Layers:
Image Signing with Cosign:
123456789101112131415161718192021222324252627282930313233343536
# Generate key pair (or use keyless with OIDC)cosign generate-key-pair # In CI/CD pipeline after build and security scan pass:# Sign the imagecosign sign --key cosign.key myregistry.com/myapp:1.0 # Optionally sign with keyless (uses OIDC identity)cosign sign myregistry.com/myapp:1.0 # Uses ambient OIDC token # Before deployment, verify the signaturecosign verify --key cosign.pub myregistry.com/myapp:1.0 # Kubernetes policy enforcement (using Kyverno)apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: verify-image-signaturesspec: validationFailureAction: enforce rules: - name: verify-signature match: resources: kinds: - Pod verifyImages: - imageReferences: - "myregistry.com/*" attestors: - entries: - keys: publicKeys: |- -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0C... -----END PUBLIC KEY-----In 2020 and beyond, supply chain attacks became mainstream threats. Attackers who compromise a registry can inject malicious code that propagates to every deployment. Defense requires: strong access controls, vulnerability scanning, image signing, and admission control policies that verify signatures before deployment.
Real-world organizations typically interact with multiple registries: public registries for base images, private registries for proprietary code, and perhaps separate registries per environment or team. Managing this complexity requires clear patterns.
Common Multi-Registry Patterns:
| Pattern | Description | Benefits |
|---|---|---|
| Hub and Spoke | Central registry pushes to regional replicas | Centralized control, local performance |
| Environment Separation | dev/staging/prod have separate registries | Isolation, different retention policies |
| Pull-Through Proxy | Single registry caches and proxies external images | Consolidation, rate limit mitigation |
| Mirror Pattern | Internal mirrors of external registries | Air-gapped networks, compliance |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
# GitHub Actions example with multiple registriesname: Build and Deploy on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Login to multiple registries - name: Login to ECR uses: aws-actions/amazon-ecr-login@v2 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Build once - name: Build and tag run: | docker build -t myapp:${{ github.sha }} . # Tag for development (GHCR) docker tag myapp:${{ github.sha }} ghcr.io/${{ github.repository }}/myapp:${{ github.sha }} docker tag myapp:${{ github.sha }} ghcr.io/${{ github.repository }}/myapp:latest # Tag for production (ECR) docker tag myapp:${{ github.sha }} ${{ env.ECR_REGISTRY }}/myapp:${{ github.sha }} # Push to development registry (always) - name: Push to GHCR run: | docker push ghcr.io/${{ github.repository }}/myapp --all-tags # Push to production registry (only on release) - name: Push to ECR if: startsWith(github.ref, 'refs/tags/v') run: | docker push ${{ env.ECR_REGISTRY }}/myapp:${{ github.sha }}Image Promotion Pattern:
A common practice is promoting images through environments rather than rebuilding:
[Build] → [Dev Registry] → [Staging Registry] → [Production Registry]
Same digest throughout - only the location changes
This ensures the exact image tested in staging is what runs in production—no "works in staging, fails in prod" because of build-time differences.
Tools like 'crane' (Google), 'skopeo' (Red Hat), and 'regctl' (regclient) enable copying images between registries without pulling to local Docker. This is efficient for automation and doesn't require Docker daemon: 'crane copy source-registry/image:tag dest-registry/image:tag'
Let's consolidate our understanding of container registries:
What's next:
With a deep understanding of how images are stored and distributed, we'll conclude this module by examining Containers vs VMs—a detailed comparison of the two dominant virtualization technologies, when to use each, and how they often complement each other in modern infrastructure.
You now understand container registries from architecture to operations. This knowledge is essential for running reliable, secure, and performant containerized infrastructure at any scale.