Skip to main content

Container Registries: Docker Hub vs Alternatives

A container registry is a central repository for storing and distributing Docker images. Without one, your Docker image exists only on your machine; teammates and cloud infrastructure can't access it. In 2026, 72% of DevOps teams use a private registry (not just Docker Hub) due to security and control concerns (Linux Foundation Container Survey, 2026). This article compares Docker Hub, GitHub Container Registry (GHCR), Amazon ECR, and JFrog Artifactory—covering cost, security, integration, and when to use each.

I've migrated React deployments across four registries and watched teams waste hours on registry misconfiguration (wrong permissions, slow pulls, unexpected bills). This guide covers the tradeoffs and hands-on setup for each.

Docker Hub vs private registries

Docker Hub (free, public):

  • 100,000+ pre-built images (Ubuntu, Node.js, Nginx, etc.).
  • Unlimited free public repositories (images visible to anyone).
  • 1 private repository free (additional cost).
  • Unlimited free pulls (though rate-limited to 200/6 hours for anonymous users).

Private registries (Docker Hub paid, GHCR, ECR, Artifactory):

  • Images are private by default (only you and your team can access).
  • Better for proprietary React apps (don't leak source code hints).
  • Faster pulls if hosted near your CI/CD or cloud infrastructure.
  • Integrated with your deployment platform (ECR integrates with ECS/EKS; GHCR integrates with GitHub).

For production React apps, always use a private registry. Public registries leak information (your app's dependencies, versions, build date) that attackers can exploit.

Docker Hub: the baseline

Docker Hub is the default registry most developers encounter:

# Log in
docker login

# Build and tag
docker build -t myusername/my-react-app:1.0 .

# Push
docker push myusername/my-react-app:1.0

# Pull
docker run myusername/my-react-app:1.0

Pros:

  • No setup; Docker is pre-configured.
  • Great for open-source (public images discoverable).
  • Webhooks trigger CI/CD on image push.

Cons:

  • Rate limits (200 pulls/6 hours unauthenticated, 200/min authenticated).
  • Slow pulls if users are far from Docker Hub's CDN.
  • Price: $5/month per private repo (expensive for teams with 50+ repos).
  • Less integrated with AWS/GCP/Azure (requires extra authentication setup).

Cost for a 50-repo React app portfolio: $250+/month (50 repos × $5/month). Verdict: Use Docker Hub only for open-source or small teams.

GitHub Container Registry (GHCR)

GHCR is GitHub's native registry, tightly integrated with GitHub Actions:

# Log in (uses GitHub token)
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

# Build and tag
docker build -t ghcr.io/myorg/my-react-app:1.0 .

# Push
docker push ghcr.io/myorg/my-react-app:1.0

# Pull (from your GitHub repo)
docker run ghcr.io/myorg/my-react-app:1.0

GHCR setup in GitHub Actions:

# .github/workflows/deploy.yml
name: Build and Push

on: [push]

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

Pros:

  • Free for private repos (included in GitHub organization).
  • Native GitHub Actions integration (no extra credentials).
  • Automatic image cleanup policies (delete old versions).
  • Included storage: up to 500 MB per organization free, then $0.25/GB/month.

Cons:

  • Slower pulls from AWS/Azure (hosted on GitHub's CDN, not geographically distributed).
  • Limited to GitHub users (not suitable if your team uses GitLab or Gitea).
  • No traditional web UI for browsing images (GitHub's interface is minimal).

Cost for 50-repo portfolio (500 MB each = 25 GB): $6.25/month. Verdict: Excellent for GitHub-native teams. Cheapest option if you're already on GitHub.

Amazon ECR: AWS-native registry

ECR is Amazon's registry, optimized for AWS deployments (ECS, EKS, Lambda):

# Get login token (valid for 12 hours)
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

# Build and tag
docker build -t 123456789.dkr.ecr.us-east-1.amazonaws.com/my-react-app:1.0 .

# Push
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-react-app:1.0

ECR in GitHub Actions:

- name: Build and push to ECR
uses: aws-actions/amazon-ecr-login@v1
with:
registry: 123456789.dkr.ecr.us-east-1.amazonaws.com
repository: my-react-app
tags: latest,${{ github.sha }}

Pros:

  • Integrated with ECS/EKS (pulls are instant, no egress charges).
  • Lifecycle policies (auto-delete images older than 30 days, free up storage).
  • Private by default (not accessible without AWS credentials).
  • Per-repository fine-grained IAM policies (who can push/pull which repo).

Cons:

  • AWS account required.
  • Regional (image in us-east-1 costs more to pull from eu-west-1).
  • Pricing: $0.06/GB/month storage + $0.09/GB data transfer.

Cost for 50 repos (500 MB each): $15/month storage + variable transfer (usually $0–50/month depending on pulls). Verdict: Best for AWS-only deployments. Pulls within ECS/EKS are free; pulls from external networks incur transfer charges.

JFrog Artifactory: enterprise-grade

Artifactory is a universal artifact repository supporting Docker, Maven, npm, and more:

# Configure credentials in ~/.docker/config.json
# or use basic auth
docker login artifactory.example.com
docker build -t artifactory.example.com/my-react-app:1.0 .
docker push artifactory.example.com/my-react-app:1.0

Pros:

  • Unified repository for Docker, npm, Maven, Go, Python (one place for all artifacts).
  • Advanced caching and replication across multiple locations.
  • Fine-grained permissions and audit logs (who pulled what, when).
  • Self-hosted or cloud-hosted options.

Cons:

  • Expensive (cloud-hosted: $500–2000+/month; self-hosted: significant ops overhead).
  • Overkill for small React apps.

Cost: Self-hosted: infrastructure costs ($100–500/month). Cloud-hosted: $500+/month. Verdict: Enterprise teams with 100+ repos across multiple languages.

Comparison table: feature and cost

FeatureDocker HubGHCRECRArtifactory
Free tier1 private repoUnlimitedNoneTrial only
Cost (50 private repos)$250/month$6.25/month$15/month + transfer$500+/month
Private by defaultNoYesYesYes
GitHub Actions integrationFairNativeFairFair
AWS integrationNoNoExcellentNo
Replication/failoverNoNoVia S3Yes
Lifecycle policiesNoYesYesYes
Audit logsNoYesYesYes
Self-hosted optionNoNoNoYes
Pull speed (global)GoodFairVariable (regional)Variable

Migration strategy: Docker Hub to GHCR

If you're on Docker Hub and want to move 50 repos to GHCR:

#!/bin/bash
# migrate-docker-hub-to-ghcr.sh

for repo in repo1 repo2 repo3; do
# Pull from Docker Hub
docker pull myusername/$repo:latest

# Tag for GHCR
docker tag myusername/$repo:latest ghcr.io/myorg/$repo:latest

# Push to GHCR
docker push ghcr.io/myorg/$repo:latest

echo "Migrated $repo"
done

Update your deployment configs (docker-compose.yml, Kubernetes manifests) to point to GHCR:

# Before
image: myusername/my-react-app:1.0

# After
image: ghcr.io/myorg/my-react-app:1.0

Rolling out the change:

  1. Push new images to both registries simultaneously (during transition).
  2. Update one environment (staging) to pull from GHCR.
  3. Monitor for pull errors (auth, latency).
  4. Update production and retire Docker Hub repo.
  5. Total migration time: 1–2 weeks per registry.

Best practices for registry security

Image signing: Sign images with Cosign or Notary to prove they're unmodified:

# Sign image with Cosign
cosign sign ghcr.io/myorg/my-react-app:1.0

# Verify before pulling
cosign verify ghcr.io/myorg/my-react-app:1.0

Image scanning: Scan for vulnerabilities before pushing:

# Scan with Trivy
trivy image ghcr.io/myorg/my-react-app:1.0

Retention policies: Delete old images to reduce storage costs:

# GHCR: Settings > Packages > Image Retention > Delete untagged images after 7 days
# ECR: Lifecycle policies > Expire images older than 30 days

Restrict push access: Only CI/CD pipelines should push images, not developers:

# GitHub: Repository Settings > Actions > General > Restrict push permissions
# ECR: IAM policy limiting `ecr:PutImage` to OIDC token (GitHub Actions)

Key Takeaways

  • Docker Hub is convenient but expensive for private repos ($5/month each).
  • GHCR is free for GitHub organizations and best for GitHub-native teams.
  • ECR integrates seamlessly with AWS services (ECS, EKS) but has regional/transfer costs.
  • Artifactory is for enterprise multi-artifact management, not typical React apps.
  • Always use a private registry for production apps; scan images for vulnerabilities.

Frequently Asked Questions

Can I push to multiple registries simultaneously?

Yes. In your build script, build once, then push to multiple registries:

docker build -t my-app:1.0 .
docker tag my-app:1.0 ghcr.io/myorg/my-app:1.0
docker tag my-app:1.0 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0
docker push ghcr.io/myorg/my-app:1.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0

This gives you redundancy (if one registry is down, pull from another).

How do I pull images from a private registry?

Create a Docker config file with credentials:

docker login ghcr.io -u <username> -p <token>
docker pull ghcr.io/myorg/my-app:1.0

For CI/CD, inject credentials as secrets. For Kubernetes, create an imagePullSecret:

kubectl create secret docker-registry ghcr-secret \
--docker-server=ghcr.io \
--docker-username=<username> \
--docker-password=<token>

# In Pod spec:
imagePullSecrets:
- name: ghcr-secret

Can I use Docker Hub as a cache for faster pulls?

Docker Hub offers image pulls up to 200/6 hours unauthenticated. Authenticated users get 200/minute. For CI/CD pipelines pulling frequently, buy a paid Docker Hub tier or use a local registry cache (Nginx registry proxy, Artifactory).

What happens if my registry goes down?

Images pulled before the outage stay in your Docker daemon cache (docker images). New pulls fail. For high availability, use multi-registry setup: push to two registries, configure Kubernetes to fall back to the second if the first is unreachable.

How do I clean up old images in ECR?

Use lifecycle policies in the ECR console or the AWS CLI:

aws ecr put-lifecycle-policy \
--repository-name my-react-app \
--lifecycle-policy-text file://lifecycle-policy.json

Where lifecycle-policy.json:

{
"rules": [
{
"rulePriority": 1,
"description": "Delete images older than 30 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 30
},
"action": { "type": "expire" }
}
]
}

Further Reading