Docker Registry: Hub, ECR, GHCR and Private Registries (2026)

A container registry stores and distributes Docker images. Docker Hub is the default public registry, but production teams typically use private registries to keep images internal, enforce access control, and reduce pull latency. AWS ECR integrates with IAM and ECS; GitHub Container Registry (GHCR) ties into GitHub Actions workflows; self-hosted Harbor adds image scanning, replication and RBAC on your own infrastructure. This phase covers authentication, tagging strategies, retention policies, pull-through caches and choosing the right registry for your stack.

Registry Comparison

# Registry          | Cost              | Auth           | Best for
# ─────────────────── | ───────────────── | ────────────── | ─────────────────
# Docker Hub         | Free (limited)    | Docker login   | Open source, public images
#                    | $5/mo Pro         | PAT            | Small teams
# GHCR               | Free (public)     | GitHub PAT     | GitHub Actions workflows
#                    | Free (private,    | GITHUB_TOKEN   | OSS projects
#                    |  with GitHub pkg) |                |
# AWS ECR            | $0.10/GB/month    | AWS IAM / OIDC | AWS-native workloads (ECS, EKS)
#                    | Free tier: 500MB  |                | Fine-grained IAM policies
# Google GCR/AR      | $0.10/GB/month    | GCP Service    | GKE workloads
#                    |                   | Account        |
# Azure ACR          | $0.003/GB         | Azure AD       | AKS workloads
# Harbor (self-host) | Infra cost only   | LDAP/OIDC/     | On-prem, compliance, air-gap
#                    |                   | local users    | Built-in scanning + replication

# Rate limits (as of 2026):
# Docker Hub anonymous: 100 pulls/6h per IP
# Docker Hub free account: 200 pulls/6h
# Docker Hub Pro: unlimited
# GHCR, ECR, GCR: no pull rate limits

Docker Hub

# Login
docker login
# Prompts for Docker Hub username and password/PAT
# Use a Personal Access Token (PAT) — not your password — for CI

# Create a PAT at hub.docker.com → Account Settings → Security → New Access Token
# Permissions: Read (pull only), Read & Write (push), Delete

# Login with PAT in CI (never hardcode credentials)
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin

# Push to Docker Hub
docker tag myapp:latest myusername/myapp:latest
docker tag myapp:latest myusername/myapp:v1.2.3
docker push myusername/myapp:latest
docker push myusername/myapp:v1.2.3

# Organization image (for teams)
docker tag myapp:latest myorg/myapp:latest
docker push myorg/myapp:latest

# Pull public image (no auth needed)
docker pull nginx:latest
docker pull node:20-alpine

# Automated builds (Docker Hub → connect GitHub repo):
# Hub can auto-build images on push — useful for open-source base images.
# For private projects, use GHCR + GitHub Actions instead (more control).

# Check your pull usage
curl -s "https://hub.docker.com/v2/users/myusername/" -H "Authorization: Bearer $TOKEN"

GitHub Container Registry

# GHCR is the best choice for GitHub-hosted projects.
# Authentication uses GitHub PATs or the automatic GITHUB_TOKEN in Actions.

# Login with a PAT (classic or fine-grained with packages:write scope)
echo "$GITHUB_TOKEN" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin

# Image naming: ghcr.io/OWNER/IMAGE:TAG
# OWNER = your GitHub username or organization name (lowercase)
docker tag myapp:latest ghcr.io/myorg/myapp:latest
docker push ghcr.io/myorg/myapp:latest

# In GitHub Actions — automatic auth with GITHUB_TOKEN (no secrets needed!)
# .github/workflows/docker.yml
jobs:
  build:
    permissions:
      contents: read
      packages: write        # Required to push to GHCR
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}   # Auto-provided, no setup needed

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/myapp:${{ github.sha }}

# Visibility: packages inherit repo visibility by default.
# Make a package public: GitHub → Packages → Package settings → Make public
# Link package to repo: Package settings → Connect repository

# Pull from GHCR (public)
docker pull ghcr.io/myorg/myapp:latest

# Pull from GHCR (private — need to be logged in)
echo "$GITHUB_PAT" | docker login ghcr.io -u myusername --password-stdin
docker pull ghcr.io/myorg/myapp:latest

AWS ECR

# ECR integrates with IAM — no separate credentials for CI/CD on AWS.
# Use OIDC in GitHub Actions to get temporary AWS credentials (no long-lived keys).

# Create an ECR repository (one per image name)
aws ecr create-repository \
  --repository-name myapp \
  --region us-east-1 \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=AES256

# Login (credentials expire every 12 hours)
aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin \
    123456789012.dkr.ecr.us-east-1.amazonaws.com

# Tag and push
REGISTRY=123456789012.dkr.ecr.us-east-1.amazonaws.com
docker tag myapp:latest $REGISTRY/myapp:latest
docker tag myapp:latest $REGISTRY/myapp:v1.2.3
docker push $REGISTRY/myapp:latest
docker push $REGISTRY/myapp:v1.2.3

# In GitHub Actions with OIDC (no AWS access keys stored as secrets):
jobs:
  deploy:
    permissions:
      id-token: write    # Required for OIDC
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1

      - uses: aws-actions/amazon-ecr-login@v2
        id: login-ecr

      - name: Build and push
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build -t $REGISTRY/myapp:${{ github.sha }} .
          docker push $REGISTRY/myapp:${{ github.sha }}

# ECR lifecycle policy — auto-delete old images to control costs
aws ecr put-lifecycle-policy \
  --repository-name myapp \
  --lifecycle-policy '{
    "rules": [{
      "rulePriority": 1,
      "description": "Keep last 10 tagged images",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["v"],
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": { "type": "expire" }
    },{
      "rulePriority": 2,
      "description": "Delete untagged images after 1 day",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 1
      },
      "action": { "type": "expire" }
    }]
  }'

Self-Hosted Harbor

# Harbor: enterprise-grade open-source registry
# Features: RBAC, LDAP/OIDC, image scanning (Trivy), replication, webhooks, quotas

# Install with Docker Compose (Harbor ships its own compose file)
wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-online-installer-v2.11.0.tgz
tar xzvf harbor-online-installer-v2.11.0.tgz
cd harbor

# Edit harbor.yml:
# hostname: registry.example.com
# http.port: 80   (or configure HTTPS with cert paths)
# harbor_admin_password: ChangeMeNow!
# database.password: root123

./install.sh --with-trivy    # Include Trivy for image scanning

# Harbor is now at http://registry.example.com
# Default: admin / ChangeMeNow!

# Login and use like any registry
docker login registry.example.com
docker tag myapp:latest registry.example.com/myproject/myapp:latest
docker push registry.example.com/myproject/myapp:latest

# Harbor project structure:
# registry.example.com/PROJECT/IMAGE:TAG
# Projects can be public or private, with per-project member roles:
# Project Admin, Maintainer, Developer, Guest

# Enable vulnerability scanning on push (harbor.yml or project settings)
# Harbor + Trivy: scans every pushed image automatically
# View scan results in Harbor web UI → Repository → Tags → Scan

# Replication: mirror images to/from other registries
# Harbor → Administration → Replications → New Replication Rule
# Source: hub.docker.com   Target: registry.example.com/docker-mirror
# Trigger: On push / Scheduled

Tagging Strategies

# Tag strategy determines how you identify and roll back image versions.

# Strategy 1: Semantic Versioning (for libraries and versioned releases)
# ghcr.io/org/myapp:v1.2.3
# ghcr.io/org/myapp:v1.2      ← floating minor
# ghcr.io/org/myapp:v1        ← floating major
# ghcr.io/org/myapp:latest    ← always latest stable

# Strategy 2: Git SHA (for continuous deployment — full traceability)
GIT_SHA=$(git rev-parse --short HEAD)
docker tag myapp ghcr.io/org/myapp:${GIT_SHA}
# Example: ghcr.io/org/myapp:a3b4c5d
# Pros: immutable, traceable
# Cons: no human-readable version

# Strategy 3: Branch + SHA (for multi-branch CD)
BRANCH=$(git rev-parse --abbrev-ref HEAD | tr '/' '-')
GIT_SHA=$(git rev-parse --short HEAD)
# ghcr.io/org/myapp:main-a3b4c5d    ← main branch
# ghcr.io/org/myapp:feature-xyz-f1e2d3c  ← feature branch (for PR previews)

# Strategy 4: Timestamp (for compliance audit trails)
TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ)
# ghcr.io/org/myapp:20260614T143022Z

# Recommended combination for production:
# 1. Tag with SHA (immutable reference)
# 2. Tag with semver on release
# 3. Update 'latest' only on main branch
docker tag myapp ghcr.io/org/myapp:${GIT_SHA}
docker tag myapp ghcr.io/org/myapp:latest   # Only on main branch
docker push ghcr.io/org/myapp:${GIT_SHA}
docker push ghcr.io/org/myapp:latest

Retention Policies

# Images accumulate fast — a busy CI pipeline pushes 20+ images/day.
# Without retention policies, registry costs and clutter grow unbounded.

# Docker Hub: paid plans have retention policies via UI
# Settings → Repositories → [repo] → Tag Retention

# GHCR: use GitHub Actions to prune old packages
# .github/workflows/cleanup.yml
name: Cleanup old packages
on:
  schedule:
    - cron: '0 2 * * 0'   # Weekly at 2am Sunday
jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/delete-package-versions@v5
        with:
          package-name: myapp
          package-type: container
          min-versions-to-keep: 10
          delete-only-untagged-versions: true

# AWS ECR: lifecycle policies (see Phase 9 ECR section above)

# Harbor: tag retention policies via UI or API
# Project → Policy → Tag Retention → Add Rule
# "Retain the most recently pushed # tags" → 20
# "Retain tags matching" → v* (keep all semver tags)

# General retention rules to apply everywhere:
# ✅ Keep last N tagged images (e.g., 20)
# ✅ Keep all semver-tagged images (v*)
# ✅ Delete untagged/dangling images after 1 day
# ✅ Delete branch images (feature-*) after 30 days
# ✅ Never auto-delete production tags (prod, stable, v*)

Pull-Through Cache

# Pull-through cache: proxy Docker Hub (or other public registries) through
# your private registry. Benefits:
# - Avoids Docker Hub rate limits in CI
# - Faster pulls (cache close to your runners)
# - Continues working during Docker Hub outages

# Option 1: Docker daemon pull-through cache (simplest)
# /etc/docker/daemon.json on your Docker host / CI runner:
{
  "registry-mirrors": ["https://registry.example.com"]
}
# docker pull nginx:latest → actually pulls from registry.example.com/library/nginx:latest
# Falls back to Docker Hub on cache miss

# Option 2: Harbor as pull-through proxy
# Harbor → Registries → New Endpoint
# Provider: Docker Hub  URL: https://hub.docker.com
# Harbor → Projects → New Project → Proxy Cache: ✅  Endpoint: docker-hub
# Now: docker pull registry.example.com/dockerhub-mirror/library/nginx:latest

# Option 3: AWS ECR pull-through cache (for ECR-native workflows)
# ECR → Pull through cache rules → Create rule
# Registry: registry-1.docker.io   ECR prefix: docker-hub
# docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/docker-hub/library/nginx:latest

# In Dockerfile/Compose — reference the cache mirror:
FROM registry.example.com/dockerhub-mirror/library/node:20-alpine AS builder
# or use build args to make it configurable:
ARG REGISTRY_MIRROR=registry.example.com/dockerhub-mirror
FROM ${REGISTRY_MIRROR}/library/node:20-alpine AS builder
Next: Phase 10 — Docker in CI/CD covers GitHub Actions and CI pipelines for automated build, test, scan and push workflows with caching strategies that keep builds fast.