Container Registry Tools: Harbor, GHCR, and Self-Hosted Options
Container Registry Tools: Harbor, GHCR, and Self-Hosted Options
Every container needs somewhere to live between being built and being deployed. Docker Hub was the default for years, but pull rate limits, security requirements, and the rise of GitOps have made the registry decision more nuanced. The right choice depends on your team size, security posture, deployment targets, and willingness to operate infrastructure.
This guide covers the major options -- managed and self-hosted -- with honest trade-offs for each.
The Registry Landscape
A container registry stores and distributes OCI (Open Container Initiative) images. At minimum, it handles docker push and docker pull. Beyond that, registries differ in access control, vulnerability scanning, image signing, replication, and pricing.
| Registry | Type | Free Tier | Private Repos | Vulnerability Scanning | Image Signing |
|---|---|---|---|---|---|
| Docker Hub | Managed | 1 private repo, rate-limited pulls | Paid ($5+/mo) | Docker Scout | Content Trust |
| GitHub Container Registry | Managed | Unlimited private (with GitHub plan) | Yes | GitHub Advanced Security | Sigstore |
| AWS ECR | Managed | 500 MB/mo free tier | Yes | Built-in (Basic + Enhanced) | Notation/Cosign |
| Google Artifact Registry | Managed | 500 MB/mo free tier | Yes | Container Analysis | Cosign |
| Harbor | Self-hosted | N/A (free OSS) | Yes | Trivy/Clair integration | Cosign/Notation |
| Gitea Container Registry | Self-hosted | N/A (free OSS) | Yes | No (external only) | No |
Docker Hub: The Default That Shows Its Age
Docker Hub is where most public images live. If you docker pull nginx, it comes from Docker Hub. For public image distribution, it remains the standard. For private use, the picture is less appealing.
Pull rate limits are the biggest pain point. Anonymous pulls are limited to 100 per 6 hours per IP address. Authenticated free users get 200 pulls per 6 hours. In a CI environment where multiple builds share an IP, you will hit these limits. Rate limit errors look like this:
ERROR: toomanyrequests: You have reached your pull rate limit.
The workaround is a pull-through cache or mirroring images to your own registry. Docker Hub's paid plans ($5/month for Pro, $9/seat for Team) remove rate limits and add more private repositories.
When Docker Hub makes sense: Publishing public images, or small teams that don't hit rate limits. For everything else, there are better options.
GitHub Container Registry (GHCR)
GHCR is tightly integrated with GitHub. Images are linked to repositories or organizations, permissions follow your existing GitHub access model, and GitHub Actions can push without extra credentials.
Setup
Enable GHCR by pushing an image. No separate account or configuration needed.
# Authenticate with GitHub
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Tag and push
docker build -t ghcr.io/myorg/myapp:latest .
docker push ghcr.io/myorg/myapp:latest
In GitHub Actions, authentication is automatic:
# .github/workflows/build.yml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
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 }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
Visibility and Access Control
GHCR packages can be public (free, unlimited pulls) or private (counts against your GitHub plan's storage). Permissions can be inherited from the linked repository or set independently at the package level.
# Make a package public (via GitHub CLI)
gh api -X PUT /user/packages/container/myapp/visibility \
-f visibility=public
Strengths: Zero-config in GitHub Actions, no pull rate limits for public images, permissions tied to your existing GitHub org, free for public packages.
Weaknesses: No built-in vulnerability scanning without GitHub Advanced Security (paid), no replication to other regions, storage costs for private images on free plans are limited.
AWS ECR: The Enterprise Choice
Amazon Elastic Container Registry is the natural choice if you deploy to ECS, EKS, or Lambda. It integrates with IAM for access control and has two scanning modes.
# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
# Create a repository
aws ecr create-repository --repository-name myapp \
--image-scanning-configuration scanOnPush=true
# Push
docker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
Lifecycle Policies
ECR's lifecycle policies automatically clean up old images, which prevents storage costs from growing unbounded:
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 tagged images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["v"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Remove untagged images after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
}
]
}
Vulnerability Scanning
ECR offers two scanning modes:
- Basic scanning uses Clair (open source) and scans on push. Free but limited to OS-level vulnerabilities.
- Enhanced scanning uses Amazon Inspector and provides continuous scanning, covering both OS and programming language vulnerabilities. Costs $0.09 per image per rescan.
Strengths: Deep AWS integration, lifecycle policies, cross-region replication, IAM-based access, enhanced scanning.
Weaknesses: Complex authentication (tokens expire every 12 hours), AWS lock-in, no public image hosting (ECR Public is a separate service with a different API), costs add up at scale.
Harbor: The Self-Hosted Powerhouse
Harbor is a CNCF graduated project and the most feature-complete open source container registry. If you need enterprise registry features without vendor lock-in, Harbor is the answer.
Installation
Harbor installs via Docker Compose or Helm chart. The Docker Compose method is simpler for getting started:
# Download Harbor installer
curl -sL https://github.com/goharbor/harbor/releases/download/v2.12.0/harbor-offline-installer-v2.12.0.tgz | tar xz
cd harbor
# Configure
cp harbor.yml.tmpl harbor.yml
Edit harbor.yml with your settings:
hostname: registry.example.com
# HTTPS configuration (required for production)
https:
port: 443
certificate: /etc/ssl/certs/registry.crt
private_key: /etc/ssl/private/registry.key
# Change the default admin password
harbor_admin_password: change-this-immediately
# Database configuration
database:
password: change-this-too
max_idle_conns: 100
max_open_conns: 900
# Storage backend (default is local filesystem)
storage_service:
filesystem:
maxthreads: 100
# Trivy vulnerability scanner
trivy:
ignore_unfixed: false
skip_update: false
security_check: vuln
# Install
sudo ./install.sh --with-trivy
# Verify
docker compose ps
curl https://registry.example.com/api/v2.0/health
Harbor Features Worth Knowing
Project-based access control. Harbor organizes images into projects. Each project has its own set of members, robot accounts, and policies. This maps well to team or application boundaries.
Vulnerability scanning with Trivy. Every image pushed to Harbor can be automatically scanned. You can set policies to prevent pulling images with critical vulnerabilities:
# Pull will fail if image has critical CVEs (when policy is enabled)
docker pull registry.example.com/myproject/myapp:latest
# Error: image cannot be pulled due to configured policy
Replication. Harbor can replicate images to and from other registries -- including Docker Hub, GHCR, ECR, and other Harbor instances. This is useful for multi-region deployments or air-gapped environments.
Image signing with Cosign. Harbor integrates with Sigstore's Cosign for keyless image signing:
# Sign an image
cosign sign registry.example.com/myproject/myapp:latest
# Verify
cosign verify registry.example.com/myproject/myapp:latest
Strengths: Full feature set (scanning, signing, replication, RBAC, audit logs), no vendor lock-in, CNCF graduated, extensive policy engine.
Weaknesses: Operational overhead (you run the database, storage, and Harbor itself), requires TLS certificates, resource-intensive (needs at minimum 4 GB RAM, 2 CPUs, 40 GB disk).
Gitea Container Registry
If you already run Gitea for Git hosting, its built-in container registry is the simplest path to private image storage. Since Gitea 1.20, the container registry is enabled by default.
# Push to Gitea registry
docker login gitea.example.com
docker tag myapp:latest gitea.example.com/myorg/myapp:latest
docker push gitea.example.com/myorg/myapp:latest
Gitea's registry is functional but minimal. It handles push, pull, and basic access control. There is no built-in vulnerability scanning, no replication, and no image signing. For small teams that want a private registry with minimal overhead, it works. For anything requiring security features, pair it with an external scanner or use Harbor instead.
Image Signing and Supply Chain Security
Image signing verifies that the image you pull is the one that was actually built. Without signing, a compromised registry (or a man-in-the-middle attack) could serve a tampered image.
Cosign (Sigstore)
Cosign is the modern standard for image signing. It supports keyless signing (using OIDC identity from CI providers) and key-based signing.
# Install cosign
brew install cosign # macOS
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Keyless signing in GitHub Actions (uses OIDC)
cosign sign ghcr.io/myorg/myapp@sha256:abc123...
# Key-based signing
cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/myorg/myapp@sha256:abc123...
# Verify
cosign verify --key cosign.pub ghcr.io/myorg/myapp@sha256:abc123...
In a GitHub Actions workflow:
- name: Sign image with Cosign
uses: sigstore/cosign-installer@v3
- name: Sign
env:
COSIGN_EXPERIMENTAL: "true"
run: cosign sign ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
Keyless signing is preferred in CI because there are no keys to manage -- identity is verified through the CI provider's OIDC token.
Kubernetes Admission Control
Image signing is most valuable when enforced at deployment time. Kyverno and OPA Gatekeeper can reject unsigned or unverified images:
# Kyverno policy to require signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign-signature
match:
resources:
kinds: ["Pod"]
verifyImages:
- imageReferences: ["ghcr.io/myorg/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
When to Self-Host vs Use Managed
Use managed registries when:
- Your team is small (under ~20 developers)
- You deploy to a single cloud provider (use their native registry)
- You don't have dedicated infrastructure/platform engineers
- Compliance requirements are met by the managed offering
- You want to minimize operational overhead
Self-host when:
- You have air-gapped or restricted network environments
- You need cross-cloud replication without vendor lock-in
- Pull rate limits from managed registries are a real problem
- You need advanced policy enforcement (vulnerability gates, signing requirements)
- You already run Kubernetes and have the operational capacity
Practical Recommendations
For most teams, the decision tree is straightforward:
GitHub-centric workflow? Use GHCR. Zero friction with GitHub Actions, free for public images, and permissions follow your existing org structure.
All-in on AWS? Use ECR. The IAM integration and lifecycle policies justify the AWS coupling. Enable enhanced scanning if you need language-level vulnerability detection.
Multi-cloud or on-prem? Run Harbor. It is the most capable self-hosted option and interoperates with every major registry through replication.
Already running Gitea? Use its built-in registry for basic private image storage, but add Trivy scanning externally.
Publishing open source images? Docker Hub is still the convention for public discovery, but GHCR is a strong alternative with no rate limits.
Whatever you choose, adopt image signing early -- it is far easier to set up from the beginning than to retrofit into an existing pipeline. And always set up lifecycle policies or garbage collection. Registries accumulate stale images quickly, and storage costs (or disk usage) grow silently until someone notices.