Skip to main content

Docker Hub vs ECR vs ACR vs GHCR — Private Registries Explained

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

You have built a Docker image. Now what? You need somewhere to store it so your CI/CD pipeline, your Kubernetes cluster, or your teammates can pull it. That somewhere is a container registry. But which one? The answer depends on your cloud provider, team size, budget, and how much you enjoy YAML.

What Is a Container Registry?

A container registry is a server that stores and distributes Docker images. Think of it like npm for Node packages or PyPI for Python — but for container images. Every registry supports the OCI (Open Container Initiative) distribution spec, which means docker push and docker pull work the same way regardless of which registry you use.

The key difference between registries is where they run, how they authenticate, and what they charge you.

Docker Hub: The Default

Docker Hub is where Docker pulls from by default. It is the largest public registry with millions of images.

# Pull from Docker Hub (implicit registry)
docker pull nginx:alpine
# This is actually: docker pull docker.io/library/nginx:alpine

# Login to Docker Hub
docker login

# Tag and push to your Docker Hub account
docker tag myapp:latest yourusername/myapp:1.0.0
docker push yourusername/myapp:1.0.0

Docker Hub's free tier has limitations that matter at scale:

  • Free: 1 private repo, unlimited public repos, 200 pull requests per 6 hours (anonymous), 200 per 6 hours (authenticated free)
  • Pro ($5/mo): Unlimited private repos, 5,000 pulls per day, vulnerability scanning
  • Team ($9/user/mo): Team management, audit logs, role-based access

The rate limiting hit many CI/CD pipelines in 2020 when Docker introduced pull limits. If your builds suddenly fail with "Too Many Requests," this is why.

AWS ECR: Native for AWS Workloads

Amazon Elastic Container Registry integrates tightly with ECS, EKS, Lambda, and IAM.

# Authenticate Docker to ECR (token valid for 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

# Create a repository (required before pushing)
aws ecr create-repository --repository-name myapp \
--image-scanning-configuration scanOnPush=true

# Tag and push
docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0.0
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0.0

# List images in a repository
aws ecr list-images --repository-name myapp

ECR pricing: $0.10/GB/month for storage, $0.09/GB for data transfer out. No per-pull charges. For AWS-native workloads (ECS, EKS), pulls within the same region are free.

ECR also offers lifecycle policies to automatically clean up old images:

# Delete untagged images older than 14 days
aws ecr put-lifecycle-policy --repository-name myapp \
--lifecycle-policy-text '{
"rules": [{
"rulePriority": 1,
"description": "Expire untagged images older than 14 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 14
},
"action": { "type": "expire" }
}]
}'

Azure ACR: Native for Azure Workloads

Azure Container Registry works seamlessly with AKS, App Service, and Azure Functions.

# Create an ACR instance
az acr create --name myregistry --resource-group mygroup --sku Basic

# Login
az acr login --name myregistry

# Tag and push
docker tag myapp:latest myregistry.azurecr.io/myapp:1.0.0
docker push myregistry.azurecr.io/myapp:1.0.0

# List repositories
az acr repository list --name myregistry --output table

# Show image tags
az acr repository show-tags --name myregistry --repository myapp

ACR tiers: Basic ($0.167/day), Standard ($0.667/day), Premium ($1.667/day). Premium adds geo-replication, content trust, and private endpoints.

GitHub Container Registry (GHCR)

GHCR integrates directly with GitHub repositories and Actions. If your code is on GitHub, this is the path of least resistance.

# Login with a personal access token (needs write:packages scope)
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

# Tag and push
docker tag myapp:latest ghcr.io/yourusername/myapp:1.0.0
docker push ghcr.io/yourusername/myapp:1.0.0

In GitHub Actions, authentication is automatic:

# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

GHCR pricing: Free for public images. Private images use your GitHub Actions storage quota (free tier includes 500 MB).

Self-Hosted Registry

You can run your own registry using Docker's official registry image. This gives you full control and zero external dependencies.

# Start a local registry
docker run -d -p 5000:5000 --name registry \
-v registry-data:/var/lib/registry \
--restart always \
registry:2

# Tag and push to local registry
docker tag myapp:latest localhost:5000/myapp:1.0.0
docker push localhost:5000/myapp:1.0.0

# Pull from local registry
docker pull localhost:5000/myapp:1.0.0

# List repositories (API call)
curl http://localhost:5000/v2/_catalog

For production self-hosted registries, add TLS and authentication:

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

volumes:
registry-data:
# Generate htpasswd file for basic auth
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin secretpass > auth/htpasswd

Image Tagging Strategies

How you tag images directly affects your deployment reliability and debugging ability.

StrategyExampleProsCons
Semantic versionmyapp:1.2.3Clear release identityManual version bumps
Git SHAmyapp:a1b2c3dExact code traceabilityHard to read
Bothmyapp:1.2.3 + myapp:a1b2c3dBest of both worldsTwo tags per build
Branch + SHAmyapp:main-a1b2c3dIdentifies branch + commitVerbose
latestmyapp:latestConvenientUnreliable, unclear version

The recommended approach is to tag with both a semantic version and the git SHA:

VERSION="1.2.3"
GIT_SHA=$(git rev-parse --short HEAD)

docker build -t myapp:${VERSION} -t myapp:${GIT_SHA} -t myapp:latest .
docker push myapp:${VERSION}
docker push myapp:${GIT_SHA}
docker push myapp:latest

Never deploy latest to production. It tells you nothing about what version is running, and pulling latest on different nodes at different times can give you different images.

Registry Comparison Table

FeatureDocker HubAWS ECRAzure ACRGHCRSelf-Hosted
Free tier1 private repoNoneNone500 MB (private)Free (you host)
Public imagesUnlimited freeNot designed for publicNot designed for publicUnlimited freeYour bandwidth
Auth integrationDocker IDIAMAzure ADGitHub tokenshtpasswd / LDAP
Vulnerability scanningPro planBuilt-in (free)Standard+ tierDependabotManual tooling
Geo-replicationNoCross-region copiesPremium tierCDN-backedManual setup
Best forOpen source, small teamsAWS-native workloadsAzure-native workloadsGitHub-centric teamsAir-gapped / on-prem
CI/CD integrationAnyAWS CodeBuild, ActionsAzure DevOps, ActionsGitHub Actions (native)Any

Wrapping Up

Your choice of registry should follow your infrastructure. On AWS? Use ECR. On Azure? Use ACR. Code on GitHub and not cloud-locked? GHCR keeps everything in one place. Need air-gapped or on-premises? Self-host. And Docker Hub remains the default for open-source images and getting started.

In the next post, we will dive into Docker Debugging — how to figure out why your container will not start, how to get inside running containers, and the essential commands that turn frustrating container problems into quick fixes.