56.8 Container registries: GHCR, Harbor, mirrors and Docker Hub limits
Why: the problem
Suppose we are at Zomato and CI suddenly fails on 30 jobs at once with: toomanyrequests, you have reached your pull rate limit. Every job pulls node and postgres from Docker Hub, all from the same office IP. At the same time the security team asks: where do our private images live, who can push, and are they scanned? A container registry is where images are stored and served. Docker Hub is the default public one. Clouds have their own: ECR on AWS, Artifact Registry on Google Cloud, ACR on Azure. GitHub has GitHub Container Registry, ghcr.io, next to the code. Harbor is an open-source registry we host ourselves, with projects, RBAC, scanning, replication and proxy caching. A pull-through mirror caches Docker Hub images close to our servers so we pull each image once. In this lesson we push to GHCR, run a local registry as a Docker Hub mirror, and see what Harbor adds for a company.
What: every term in plain English
- Registry — Server that stores and serves images (OCI distribution API).
- Repository — One image name in a registry, with many tags.
- Docker Hub limits — Anonymous and free accounts have pull rate limits per IP or user; check current numbers in Docker's docs.
- GHCR — ghcr.io, GitHub's registry; auth with a token.
- ECR / Artifact Registry / ACR — Cloud registries tied to IAM.
- Harbor — Self-hosted CNCF registry: projects, RBAC, Trivy scan, replication, proxy cache.
- Pull-through cache — Registry that fetches from upstream once and serves local copies.
- registry-mirrors — Daemon setting that sends Docker Hub pulls to a mirror.
- Insecure registry — HTTP registry; only for labs.
How: step by step
- Check your rate limit. Read the ratelimit headers.
- Log in safely. --password-stdin.
- Tag with the registry host. ghcr.io/org/app:sha.
- Push and pull. Verify by digest.
- Run a mirror. registry:2 with REGISTRY_PROXY_REMOTEURL.
- Point the daemon at it. registry-mirrors in daemon.json.
- Plan for a company. Harbor or a cloud registry with scanning and RBAC.
Example 1: See the Docker Hub rate limit, then push to GHCR
Goal. See the Docker Hub rate limit, then push to GHCR.
Commands
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -s --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimit
read -s GHCR_TOKEN
echo "$GHCR_TOKEN" | docker login ghcr.io -u ravi-dev --password-stdin
unset GHCR_TOKEN
docker tag shop-api:a1b2c3d ghcr.io/ravi-dev/shop-api:a1b2c3d
docker push ghcr.io/ravi-dev/shop-api:a1b2c3d
docker buildx imagetools inspect ghcr.io/ravi-dev/shop-api:a1b2c3d | head -3
What each line does
ratelimitpreview/test— Docker's test repository for reading limits.ratelimit-limit / remaining— Our quota and what is left for this window.read -s— Type the token without echo or shell history.--password-stdin— Token never appears in ps output.ghcr.io/USER/NAME:TAG— The host part decides the registry.imagetools inspect— Confirms the pushed digest.
Green output
ratelimit-limit: 100;w=21600
ratelimit-remaining: 97;w=21600
Login Succeeded
a1b2c3d: digest: sha256:4c1e...9ab size: 1784
Name: ghcr.io/ravi-dev/shop-api:a1b2c3d
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:4c1e...9ab
Expected result. We can see exactly how many pulls remain, and our image now lives in GHCR next to the code. Numbers shown are an example; real limits depend on the account type.
Example 2: Run a pull-through mirror for Docker Hub
Goal. Run a pull-through mirror for Docker Hub.
mirror/compose.yaml
services:
mirror:
image: registry:2.8
ports: ["5000:5000"]
environment:
REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- mirror-data:/var/lib/registry
restart: unless-stopped
volumes:
mirror-data: {}
/etc/docker/daemon.json
{
"registry-mirrors": ["http://10.0.0.5:5000"],
"insecure-registries": ["10.0.0.5:5000"]
}
Commands
cd mirror && docker compose up -d && cd ..
sudo systemctl restart docker
docker info | grep -A1 'Registry Mirrors'
docker pull node:22-slim
curl -s http://10.0.0.5:5000/v2/_catalog
docker rmi node:22-slim && time docker pull node:22-slim
What each line does
REGISTRY_PROXY_REMOTEURL— Turns registry:2 into a cache of Docker Hub.mirror-data volume— Cached layers survive restarts.registry-mirrors— Every Docker Hub pull on this host tries the mirror first.insecure-registries— HTTP is fine only on a private lab network; use TLS in real setups._catalog— Shows what the mirror has cached.Second pull— Served from the LAN, no Hub quota used.
Green output
Registry Mirrors:
http://10.0.0.5:5000/
{"repositories":["library/node"]}
real 0m3.1s
Expected result. The first pull filled the cache; every later pull on any configured host is local and does not count against the Hub limit.
Example 3: Harbor for a company: projects, robots, scanning and proxy cache
Goal. Harbor for a company: projects, robots, scanning and proxy cache.
harbor.yml (excerpt)
hostname: harbor.zomato-lab.example.com
https:
port: 443
certificate: /data/cert/harbor.crt
private_key: /data/cert/harbor.key
harbor_admin_password: change-me-on-first-login
database:
password: change-me-db
data_volume: /data/harbor
trivy:
ignore_unfixed: true
Commands
tar xzf harbor-online-installer-v2.11.1.tgz && cd harbor
cp harbor.yml.tmpl harbor.yml
sudo ./install.sh --with-trivy
docker compose ps --format '{{.Name}} {{.Status}}' | head -5
read -s ROBOT_SECRET
echo "$ROBOT_SECRET" | docker login harbor.zomato-lab.example.com -u 'robot$ci-orders' --password-stdin
docker tag orders-api:a1b2c3d harbor.zomato-lab.example.com/orders/orders-api:a1b2c3d
docker push harbor.zomato-lab.example.com/orders/orders-api:a1b2c3d
docker pull harbor.zomato-lab.example.com/dockerhub-cache/library/postgres:16
What each line does
install.sh --with-trivy— Harbor runs as a Compose app with Trivy built in.Projects— orders is a project with its own members and quotas.robot$ci-orders— Robot account for CI, push only to orders.Scan on push— Set per project; Harbor can block pulls of images with critical CVEs.dockerhub-cache project— A proxy-cache project for Docker Hub.Replication— Copy images to ECR or another Harbor in another region.
Green output
harbor-core Up (healthy)
harbor-db Up (healthy)
harbor-jobservice Up (healthy)
harbor-portal Up (healthy)
nginx Up (healthy)
Login Succeeded
a1b2c3d: digest: sha256:7d2f...c10 size: 1572
16: Pulling from dockerhub-cache/library/postgres
Status: Downloaded newer image for harbor.zomato-lab.example.com/dockerhub-cache/library/postgres:16
Expected result. Harbor gives one place for private images, per-team access, CI robots, scanning gates and a Docker Hub cache. Install it from the Harbor docs at goharbor.io.
Every command, step by step (plain docker and Compose)
-
Where am I logged in
cat ~/.docker/config.json | jq '.auths | keys'What it does: Registries with saved creds.
["ghcr.io","harbor..."] -
Use a credential helper
docker-credential-ecr-login listWhat it does: No plain tokens in config.json.
{...} -
List tags (any OCI registry)
docker run --rm quay.io/skopeo/stable list-tags docker://ghcr.io/ravi-dev/shop-apiWhat it does: Tags without pulling.
"a1b2c3d" -
Copy between registries
docker run --rm quay.io/skopeo/stable copy docker://ghcr.io/ravi-dev/shop-api:a1b2c3d docker://123456789012.dkr.ecr.ap-south-1.amazonaws.com/shop-api:a1b2c3dWhat it does: Registry to registry.
Copying blob ... done -
ECR login
aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-south-1.amazonaws.comWhat it does: AWS registry.
Login Succeeded -
Mirror catalog
curl -s http://10.0.0.5:5000/v2/_catalogWhat it does: Cached repos.
{"repositories":[...]} -
Logout
docker logout ghcr.ioWhat it does: Remove saved creds.
Removing login credentials for ghcr.io
When it is red 1
toomanyrequests: You have reached your pull rate limit.
Cause. Docker Hub pull quota used up for this IP or account.
Fix, step by step
- Log in so limits apply per account.
- Add a pull-through mirror or Harbor proxy cache.
- Mirror base images into ECR or GHCR.
When it is red 2
denied: permission_denied: write_package
Cause. GHCR token lacks write:packages or the user is not allowed in that org.
Fix, step by step
- Create a token with write:packages.
- In CI use the workflow token with packages: write permission.
When it is red 3
http: server gave HTTP response to HTTPS client
Cause. The registry is HTTP but Docker expects HTTPS.
Fix, step by step
- Use TLS (recommended).
- For a lab only, add the host to insecure-registries and restart Docker.
Practice tasks
Easy Task 1: Limit
Why: Hub.
What to do: Read the ratelimit headers.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
curl --head ... | grep -i ratelimitEasy Task 2: Login
Why: Safe.
What to do: docker login with --password-stdin.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
echo "$T" | docker login ghcr.io -u USER --password-stdinMedium Task 3: Tag and push
Why: GHCR.
What to do: Push one image to ghcr.io.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
docker push ghcr.io/USER/app:shaMedium Task 4: Digest
Why: Verify.
What to do: Inspect the pushed digest.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
docker buildx imagetools inspect ghcr.io/USER/app:shaMedium Task 5: Mirror
Why: Cache.
What to do: Run registry:2 as a proxy.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
docker compose up -dMedium Task 6: Daemon
Why: Use it.
What to do: Add registry-mirrors.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
sudo systemctl restart dockerHard Task 7: Prove cache
Why: Test.
What to do: Show the catalog after a pull.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
curl -s http://10.0.0.5:5000/v2/_catalogHard Task 8: skopeo copy
Why: Move.
What to do: Copy an image GHCR to ECR.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
skopeo copy docker://... docker://...Hard Task 9: Harbor
Why: Company.
What to do: Install Harbor with Trivy.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
sudo ./install.sh --with-trivyEasy Task 10: Logout
Why: Hygiene.
What to do: Remove saved creds.
- Open a terminal with Docker.
- For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
- Never paste real tokens into files; use --password-stdin.
Solution
docker logout ghcr.ioHomework: try at home
At home, check your Docker Hub limit, push one image to GHCR, run a registry:2 mirror, point your daemon at it and prove the second pull is local.
Recap
- Know where your images live and who can push.
- Docker Hub has pull rate limits; log in and cache.
- GHCR, ECR, Artifact Registry and ACR tie access to your platform.
- registry:2 with REGISTRY_PROXY_REMOTEURL is a simple mirror.
- Harbor adds projects, robots, scanning and replication.
- Always --password-stdin and credential helpers.
FAQ
Q. Does registry-mirrors work for GHCR or ECR?
No, only for Docker Hub. Use Harbor proxy-cache projects for other registries.
Q. Is GHCR free?
Public images are free; private storage and transfer follow GitHub's plan pricing.
Q. Harbor or a cloud registry?
Cloud registry if you live in one cloud; Harbor for on-prem, multi-cloud or air-gapped sites.
Q. What about JFrog Artifactory or Nexus?
Both host Docker registries plus Maven, npm and more; common in large enterprises.
Q. Can I delete old images?
Use lifecycle policies (ECR), retention rules (Harbor) or GHCR package settings.
Q. Should prod pull from Docker Hub?
Better to pull from your own registry with approved, scanned copies.
Q. What is an air-gapped registry?
A registry inside a network with no internet; images are copied in with skopeo or Harbor replication after scanning.
Interview questions
-
How do you handle Docker Hub rate limits in CI?
Model answer: Authenticate, use a pull-through cache or Harbor proxy, and mirror base images into a private registry.
-
Compare ECR and Harbor.
Model answer: ECR is managed and IAM-based; Harbor is self-hosted with projects, RBAC, Trivy and replication.
-
How do CI systems log in safely?
Model answer: Short-lived tokens or OIDC, --password-stdin, credential helpers, no tokens in files.
-
What is a pull-through cache?
Model answer: A registry that fetches images from upstream once and serves them locally.
-
How do you copy images between registries?
Model answer: skopeo copy or docker buildx imagetools create, preserving digests.
-
How do you stop vulnerable images from being pulled?
Model answer: Harbor's prevent-vulnerable-images policy, or admission control in Kubernetes.
Quick quiz
Q1. Which env var turns registry:2 into a cache?
REGISTRY_PROXY_REMOTEURL.
Q2. Safest way to pass a token to docker login?
--password-stdin.
Q3. What decides which registry a tag goes to?
The host part of the name.
Q4. What does Harbor add over registry:2?
Projects, RBAC, robots, scanning, replication, UI.
Q5. How to fix Hub rate limits?
Log in, mirror or cache.
Samajla ka?
Revision card
Interview card