TL;DR #
- GitHub Packages: Free for public repos, integrates with Actions. Start here.
- Docker Hub: Still viable for public images. Private repos get expensive fast.
- ECR / Artifact Registry / ACR: Use if already on AWS/GCP/Azure. Pricing scales with usage. (Google Container Registry is gone — it stopped accepting pushes in March 2025 and stopped serving reads in June 2025. Artifact Registry replaced it.)
- Harbor: Self-host only if compliance requires images on your own infrastructure, or you need multi-tenancy.
- Don’t self-host unless you have compliance requirements or enjoy midnight registry outages.
What is a Container Registry? #
A container registry stores your Docker images. Think Git for containers.
You push images after building. Your deployment tools pull them. Simple concept, endless implementation choices.
For small teams, the decision boils down to: use someone else’s infrastructure or run your own. Spoiler: use someone else’s.
Who Should Use What #
GitHub Packages if:
- Your code lives in GitHub
- You use GitHub Actions for CI
- You want zero additional vendors
Docker Hub if:
- You publish open source images
- You need the widest compatibility
- You’re OK with their pricing for private repos
Cloud Provider Registry (ECR / Artifact Registry / ACR) if:
- You’re already all-in on AWS/GCP/Azure
- You deploy to the same cloud
- You want IAM integration
Harbor (self-hosted) if:
- Compliance requires on-premises storage
- You need multi-tenancy the managed registries do not offer
- You have spare ops capacity
Detailed Comparison #
GitHub Packages #
Free for public repos. For private images the free allowance depends on your plan — 500MB storage and 1GB transfer on Free, 2GB/10GB on Pro and Team, 50GB/100GB on Enterprise Cloud — and it is shared with GitHub Actions artifacts, which is where most teams quietly spend it.
More importantly: storage and bandwidth for the Container registry (ghcr.io) are currently free, with GitHub promising at least a month’s notice before that changes. The published rates of $0.25/GB storage and $0.50/GB transfer apply to the older Packages registries, and transfer is not billed at all when the pull comes from GitHub Actions using GITHUB_TOKEN.
Pros:
- Integrated with GitHub Actions
- Same auth as your repos
- Decent free tier
Cons:
- Tied to GitHub ecosystem
- Limited features vs dedicated registries
- The free bandwidth is a current policy, not a commitment
Docker Hub #
One private repo free. Team plan: $16/user/month billed monthly, $15 billed annually. Pro is $11/$9 for a single user.
Pros:
- The default registry
- Great for public images
- Wide tool support
Cons:
- Rate limits on pulls (100/6hr anonymous, 200/6hr authenticated)
- Gets expensive for private repos
- Recent pricing changes upset many
ECR (AWS) #
$0.10/GB/month storage. $0.09/GB transfer to the internet, after AWS’s 100GB/month free egress allowance shared across services. Transfer within the AWS region is free.
Pros:
- IAM integration
- Lifecycle policies
- Basic vulnerability scanning included (enhanced scanning runs through Amazon Inspector and is billed separately)
Cons:
- AWS-only
- No web UI worth mentioning
- Complexity for simple needs
Harbor #
Free (self-hosted). Your infrastructure costs.
Pros:
- Full control
- Built-in vulnerability scanning
- Multi-tenancy support
Cons:
- You maintain it
- Needs 4GB RAM minimum
- Another thing to monitor
Quick Setup Examples #
GitHub Packages #
# .github/workflows/build.yml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
# Declaring any permission zeroes out every scope you don't list.
# Without contents: read, checkout fails with a 403 before the
# build even starts.
contents: read
packages: write
steps:
- uses: actions/checkout@v7
# ghcr.io rejects uppercase. github.repository preserves the case of
# the owner and repo names, so Acme/My-App has to be lowercased or
# the push dies with "repository name must be lowercase".
- name: Lowercase the image name
run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v7
with:
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}ECR with Terraform #
# ecr.tf
resource "aws_ecr_repository" "app" {
name = "my-app"
image_scanning_configuration {
scan_on_push = true
}
}
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 10 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
}]
})
}
output "repository_url" {
value = aws_ecr_repository.app.repository_url
}Cost Comparison (10-Person Team) #
Assuming 50GB storage, 200GB transfer/month:
| Registry | Monthly Cost | Notes |
|---|---|---|
GitHub Packages (ghcr.io) |
$0 | Storage and bandwidth currently not billed |
| ECR | ~$14 | $5 storage + $9 transfer, after the 100GB free egress tier |
| Docker Hub | ~$160 | Team plan at $16/user for 10 people |
| Harbor | ~$40-80 | VM and storage costs, plus your time |
Three things this table hides, and they matter more than the numbers.
The egress figure only applies if you pull from outside AWS. Pulling ECR images into ECS or EKS in the same region costs nothing, which removes the entire $9. Likewise, pulling ghcr.io images from GitHub Actions is not billed even once GitHub starts charging for bandwidth.
Docker Hub is priced per seat, not per gigabyte. It gets more expensive as the team grows and stays flat as the images grow — the opposite shape from everyone else. At ten engineers it is the most expensive option here by an order of magnitude.
“Free” is a current state, not a guarantee. GitHub has committed to a month’s notice before billing container storage. Build on the assumption that it will one day cost roughly what the other registries cost.
Operations Considerations #
Availability: Cloud registries handle this. Harbor needs redundancy planning.
Access Control: All support tokens/service accounts. Cloud registries integrate with IAM.
Cleanup: Old images eat money. Set lifecycle policies. ECR and Harbor support this natively. GitHub Packages and Docker Hub need external scripts.
Monitoring: Cloud registries give basic metrics. Harbor needs Prometheus setup.
Security Notes #
All registries support image signing and vulnerability scanning. Costs vary:
- GitHub Packages: Via Actions (free minutes permitting)
- Docker Hub: Docker Scout, free on one repository for Personal and two for Pro, unlimited on Team and Business
- ECR: basic scanning included; enhanced scanning via Amazon Inspector at roughly $0.09 per initial image scan plus $0.01 per automatic rescan
- Harbor: Included (uses Trivy)
For small teams, ECR’s included scanning often tips the balance if you’re on AWS.
When to Skip Container Registries #
If you deploy to a single server and rebuild infrequently, you might not need a registry. Build locally, save to tar, scp to server.
This breaks down quickly with multiple developers or environments. But for that weekend project? YAGNI.
Migration Path #
Start with GitHub Packages or your cloud provider. Migrate only when you hit real limitations.
Moving registries is straightforward:
- Pull all images from old registry
- Re-tag for new registry
- Push to new registry
- Update all deployment configs
- Keep old registry read-only for 30 days
The hard part is updating all the references in your CI/CD pipelines and deployment manifests.
Recommendation Matrix #
- Solo project: Docker Hub free tier
- Small team, GitHub users: GitHub Packages
- Small team, AWS infrastructure: ECR
- Compliance requirements: Harbor
- Multi-cloud: Docker Hub paid or Harbor
Start simple. You can always migrate when your needs crystalize.
Questions people ask #
What is the best private Docker registry for a small team? #
Whichever one you do not have to operate. If your code is on GitHub, GitHub Packages is the default: it is already authenticated against the repositories you own and needs no additional vendor. If you are committed to AWS, GCP or Azure, use their registry so image pulls stay inside the cloud you already deploy to and IAM handles access. Self-hosting a registry is a real operational job, worth it only when compliance forces images onto your own infrastructure or when you need multi-tenancy the managed options do not give you.
What is the cheapest container registry? #
For public images, Docker Hub and GitHub Packages are free. For private images, the cheapest option is usually the registry belonging to the cloud you already run in, because you avoid cross-cloud egress on every pull — egress, not storage, is what makes registry bills surprising. Self-hosting looks cheapest on paper and stops being cheapest the first time it goes down during a deploy.
Do you need a private container registry at all? #
Less often than you would think. If you deploy to a single server and rebuild rarely, you can build locally and copy the image across without any registry at all. Teams publishing open source can live on Docker Hub indefinitely. Everyone else does need somewhere private — but “private registry” almost always means a private repository on a managed service rather than a server you maintain.
Is a self-hosted registry like Harbor worth it? #
Harbor makes sense for two reasons: compliance that requires images to stay on your own infrastructure, or multi-tenancy the managed registries do not offer. Note that “free vulnerability scanning” is no longer one of them — ECR includes basic scanning, and GitHub Packages scans through Actions. Outside those two cases it adds a stateful service to your critical deploy path, and a registry outage blocks every deployment until it is fixed.
What breaks when a container registry goes down? #
Deployments and any pod that needs to pull an image — including replacements for pods that crash while the registry is unavailable. Already-running containers keep running, which makes the outage look milder than it is until autoscaling or a node failure forces a pull. This is the main argument against self-hosting the registry your production cluster depends on.
Related Reads #
- CI/CD for Small Teams - Connecting your registry to pipelines
- GitOps with ArgoCD for Small Teams - Pull-based deployments from registries
- Minimal DevOps Stack - Where registries fit in your infrastructure