The Image Distribution Problem Registries Solve
Building a container image on your laptop is useless if others can't easily access it. Without a registry, you'd email 500MB files or manually copy images to each machine. Container registries are the distribution layer: you push once, and anyone with credentials can pull the exact same image from anywhere—production servers, CI/CD pipelines, other developers' laptops. Registries also act as a control point: you scan images before they're deployed, enforce access policies, and track what versions are running where.
A container registry is a server that stores and distributes container images. It's like GitHub for Docker images: you push images to it, others pull from it, and it becomes your central source of truth for which versions are approved for deployment.
graph TB Dev["Developer<br/>Local Build"] Dev -->|Push| Registry["Container Registry<br/>Docker Hub<br/>ECR, GCR<br/>Harbor"] Registry -->|Scan| Scanner["Security<br/>Scanner"] Scanner -->|Results| Registry Registry -->|Pull| Prod["Production<br/>Deployment"] Registry -->|Pull| Dev2["Another<br/>Developer"] Registry -->|Pull| CI["CI/CD<br/>Pipeline"] style Registry fill:#fff9c4 style Prod fill:#c8e6c9 style Dev2 fill:#c8e6c9 style CI fill:#c8e6c9How Registries Work
Think of a registry as a giant image library. On your laptop, you build myapp:1.0.0 locally. You then push this image to a Registry Server, which stores it as layers, metadata, permissions, and scanning results. Other developers can pull the same myapp:1.0.0 image from the registry onto their laptops. The registry acts as the central distribution point, ensuring everyone gets the exact same version.
Key operations include the ability to pull (download an image) with docker pull gcr.io/my-project/myapp:1.0.0, tag (mark a local image for a specific registry) with docker tag myapp:1.0.0 gcr.io/my-project/myapp:1.0.0, push (upload an image to the registry) with docker push gcr.io/my-project/myapp:1.0.0, and delete (remove from registry if you have permission) with docker rmi gcr.io/my-project/myapp:1.0.0.
Registry location indicators are important to understand. When you see ubuntu:22.04 with no registry prefix, it refers to Docker Hub (docker.io) by default. A full registry reference like gcr.io/my-project/myapp:1.0.0 breaks down as follows: gcr.io is the registry host (Google Container Registry), /my-project is the project or namespace, and /myapp:1.0.0 is the image name with its tag. Another example, registry.cleanstart.io/python:3.11-prod, uses the CleanStart registry host with the image python:3.11-prod. The pattern is always [registry]/[namespace]/[image]:[tag], where the registry part is optional and defaults to Docker Hub.
Popular Container Registries
Docker Hub (docker.io) is the default, most well-known registry. It hosts millions of public images, offers a free tier with unlimited public images and 1 private repo, and is used by 99% of developers for official images. You can search at https://hub.docker.com.
docker pull ubuntu:22.04 # Pulls from Docker Hub by defaultdocker pull docker.io/library/ubuntu:22.04 # ExplicitGoogle Artifact Registry (GCP) uses the pattern <region>-docker.pkg.dev/<project>/<repository>/<image>:<tag>. It is enterprise-grade, integrated with GCP, good for Google Cloud deployments, and supports private repos, access control, and vulnerability scanning.
# Authenticate to GCPgcloud auth configure-docker us-central1-docker.pkg.dev # Pulldocker pull us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0 # Pushdocker tag myapp:1.0.0 us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0docker push us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0AWS Elastic Container Registry (ECR) uses the pattern <account>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>. It is integrated with AWS services, good for AWS deployments, and private by default.
# Authenticate to AWSaws ecr get-login-password --region us-east-1 | \ docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com # Pushdocker tag myapp:1.0.0 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0.0docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0.0GitHub Container Registry (GHCR) uses the pattern ghcr.io/<username>/<image>:<tag>. It is free with GitHub account, integrated with GitHub Actions, and easy for open-source projects.
# Authenticateecho $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin # Pushdocker tag myapp:1.0.0 ghcr.io/myusername/myapp:1.0.0docker push ghcr.io/myusername/myapp:1.0.0CleanStart Registry uses the pattern registry.cleanstart.io/<image>:<tag>. It features pre-scanned and signed images, supply-chain verified with provenance data, hardened base images with security best practices, and immutable digests for supply chain security.
# Authenticate (CleanStart provides credentials)docker login registry.cleanstart.io # Pull verified imagesdocker pull registry.cleanstart.io/python:3.11-proddocker pull registry.cleanstart.io/node:18-proddocker pull registry.cleanstart.io/ubuntu:22.04-prod # Verify signature (with cosign)cosign verify registry.cleanstart.io/python:3.11-prodHow Images Are Stored and Versioned
Images in a registry are organized by layers. In gcr.io/my-project, the myapp repository contains multiple tags. The latest tag references layers sha256:a1b2c3 (100MB Ubuntu base), sha256:d4e5f6 (150MB Python and packages), and sha256:g7h8i9 (5MB app code). Tag 1.0.0 reuses the same three layers as latest, pointing to identical hashes. Tag 1.1.0 reuses the first two layers but has a new code layer sha256:j1k2l3. Rather than storing three complete copies of 255MB each, the registry stores only unique layers once: 100MB for the base, 150MB for packages, 5MB for the original code, and 5MB for the updated code, totaling 260MB. Layers are automatically deduplicated, so multiple tags and images can share the same underlying layers without duplication.
Metadata stored for each image includes image layers (content-addressable by hash), configuration (environment, ports, user, healthcheck), tags and digests, created date and author, scanning results (vulnerabilities detected), and signatures (cryptographic proof of authenticity).
Tags vs Digests: Which Should You Use?
This is critical for production systems.
Tags (e.g., myapp:1.0.0 or myapp:latest) are human-readable and easy to remember but are mutable—you can rebuild and push a new image with the same tag. The problem: latest can change without notice. They are good for development and easy updates but bad for production and supply chain security.
# Development: Use tag for convenienceFROM myapp:1.0.0 # Easy to read, but might change # What happens:# Jan 1: myapp:1.0.0 is image A (hash 0x123)# Jan 5: You rebuild, push with same tag# Now myapp:1.0.0 is image B (hash 0x456)# Any new containers get image B, old containers have image A# INCONSISTENCY and CONFUSIONDigests (e.g., myapp@sha256:a1b2c3d4e5f6...) are cryptographic hashes of the image contents. They are immutable—same digest always pulls the exact same image bytes. They are longer to type (nobody wants to type 64-character hashes) but guarantee consistency. They are good for production, security, and auditing but bad for human readability.
# Production: Use digest for consistencyFROM myapp@sha256:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 # Guaranteed exact image # What happens:# This ALWAYS pulls the exact same image, byte-for-byte# Even if you rebuild and push a new tag# Perfect for supply chain securityHow to find digests:
# When you push, it shows the digest$ docker push gcr.io/my-project/myapp:1.0.0The push refers to repository [gcr.io/my-project/myapp]1.0.0: digest: sha256:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 size: 2048 # Use it in DockerfileFROM gcr.io/my-project/myapp@sha256:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 # Or pull by digestdocker pull gcr.io/my-project/myapp@sha256:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6Best practice: Use tags in development, verify with digests in production.
Authentication: Access Control
Registries need to verify who you are before letting you pull or push.
Docker credentials are stored locally. To log in (one time per registry), run docker login gcr.io which prompts for username/password or token. Docker stores credentials in ~/.docker/config.json:
cat ~/.docker/config.json{ "auths": { "gcr.io": { "auth": "base64_encoded_credentials" } }}Now you can pull and push with docker push gcr.io/my-project/myapp:1.0.0.
Service account authentication is recommended for CI/CD. GCP uses service account key via gcloud auth configure-docker. AWS uses IAM roles via aws ecr get-login-password | docker login --username AWS --password-stdin <url>. GitHub uses token via echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin.
Common authentication patterns appear in CI/CD configurations like:
# GitHub Actions: Authenticate to container registryjobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Login to GHCR uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push image run: docker push ghcr.io/myorg/myapp:1.0.0Example: Complete Pull and Push Workflow
Here's a real-world scenario:
Step 1: Start with a base image (public)
# Pull from Docker Hub (free, public)docker pull ubuntu:22.04 # This goes:# docker.io (default) → library (default namespace) → ubuntu → 22.04Step 2: Extend with your code
# DockerfileFROM ubuntu:22.04RUN apt-get update && apt-get install -y python3COPY app.py /app/app.pyCMD ["python3", "/app/app.py"]Step 3: Build locally
docker build -t myapp:1.0.0 .Step 4: Tag for your registry
# If using Google Artifact Registrydocker tag myapp:1.0.0 us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0Step 5: Authenticate to registry
gcloud auth configure-docker us-central1-docker.pkg.devStep 6: Push to registry
docker push us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0 # Output:# The push refers to repository [us-central1-docker.pkg.dev/my-project/docker-repo/myapp]# 1.0.0: digest: sha256:a1b2c3... size: 4096Step 7: Verify in registry
# List images in repositorygcloud artifacts docker images list us-central1-docker.pkg.dev/my-project/docker-repo # Output:# myapp# TAG DIGEST# 1.0.0 a1b2c3...Step 8: Pull from another machine
# Authenticate firstgcloud auth configure-docker us-central1-docker.pkg.dev # Pull (now available everywhere)docker pull us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0 # Run itdocker run -it us-central1-docker.pkg.dev/my-project/docker-repo/myapp:1.0.0The Bottom Line
A registry is a central store for container images. Tags are human-readable references (mutable, for development). Digests are cryptographic hashes (immutable, for production). Layers are deduplicated to enable efficient storage and bandwidth. Authentication is required, with credentials stored locally or in CI/CD. CleanStart images are pre-scanned and signed for supply chain security.
Next Steps
Read What is a Container Image? for image structure. Read End-to-End Secure Deployment to practice pushing/pulling. Learn CycloneDX SBOM for image provenance.
Common mistakes to avoid: Using latest tag in production (it's mutable), Forgetting to authenticate (auth tokens expire), Mixing public and private registries without documentation, Storing credentials in Dockerfile or version control, and Not verifying image signatures before running.
