Ravindra BagaleCourses & study guides मराठी

Chapter 56: Docker CI/CD with Jenkins and ECR, Deploy to Amazon ECS, Troubleshooting, Best Practices and Interview Questions

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.
Check your rate limit. Log in safely. Tag with the registry host. Push and pull. Run a mirror. Point the daemon at it. Plan for a company.
Store, serve, cache and control images

How: step by step

  1. Check your rate limit. Read the ratelimit headers.
  2. Log in safely. --password-stdin.
  3. Tag with the registry host. ghcr.io/org/app:sha.
  4. Push and pull. Verify by digest.
  5. Run a mirror. registry:2 with REGISTRY_PROXY_REMOTEURL.
  6. Point the daemon at it. registry-mirrors in daemon.json.
  7. 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)

  1. Where am I logged in

    cat ~/.docker/config.json | jq '.auths | keys'
    

    What it does: Registries with saved creds.

    ["ghcr.io","harbor..."]
    
  2. Use a credential helper

    docker-credential-ecr-login list
    

    What it does: No plain tokens in config.json.

    {...}
    
  3. List tags (any OCI registry)

    docker run --rm quay.io/skopeo/stable list-tags docker://ghcr.io/ravi-dev/shop-api
    

    What it does: Tags without pulling.

    "a1b2c3d"
    
  4. 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:a1b2c3d
    

    What it does: Registry to registry.

    Copying blob ... done
    
  5. ECR login

    aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-south-1.amazonaws.com
    

    What it does: AWS registry.

    Login Succeeded
    
  6. Mirror catalog

    curl -s http://10.0.0.5:5000/v2/_catalog
    

    What it does: Cached repos.

    {"repositories":[...]}
    
  7. Logout

    docker logout ghcr.io
    

    What 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

  1. Log in so limits apply per account.
  2. Add a pull-through mirror or Harbor proxy cache.
  3. 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

  1. Create a token with write:packages.
  2. 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

  1. Use TLS (recommended).
  2. 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.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
curl --head ... | grep -i ratelimit

Easy Task 2: Login

Why: Safe.

What to do: docker login with --password-stdin.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
echo "$T" | docker login ghcr.io -u USER --password-stdin

Medium Task 3: Tag and push

Why: GHCR.

What to do: Push one image to ghcr.io.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
docker push ghcr.io/USER/app:sha

Medium Task 4: Digest

Why: Verify.

What to do: Inspect the pushed digest.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
docker buildx imagetools inspect ghcr.io/USER/app:sha

Medium Task 5: Mirror

Why: Cache.

What to do: Run registry:2 as a proxy.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
docker compose up -d

Medium Task 6: Daemon

Why: Use it.

What to do: Add registry-mirrors.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
sudo systemctl restart docker

Hard Task 7: Prove cache

Why: Test.

What to do: Show the catalog after a pull.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
curl -s http://10.0.0.5:5000/v2/_catalog

Hard Task 8: skopeo copy

Why: Move.

What to do: Copy an image GHCR to ECR.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. 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.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
sudo ./install.sh --with-trivy

Easy Task 10: Logout

Why: Hygiene.

What to do: Remove saved creds.

How:
  1. Open a terminal with Docker.
  2. For GHCR you need a GitHub account and a personal access token with write:packages; for Harbor any Linux VM with 4 GB RAM.
  3. Never paste real tokens into files; use --password-stdin.
Solution
docker logout ghcr.io

Homework: 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

  1. 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.

  2. Compare ECR and Harbor.

    Model answer: ECR is managed and IAM-based; Harbor is self-hosted with projects, RBAC, Trivy and replication.

  3. How do CI systems log in safely?

    Model answer: Short-lived tokens or OIDC, --password-stdin, credential helpers, no tokens in files.

  4. What is a pull-through cache?

    Model answer: A registry that fetches images from upstream once and serves them locally.

  5. How do you copy images between registries?

    Model answer: skopeo copy or docker buildx imagetools create, preserving digests.

  6. 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?

Ravi Sir's Cards for this lesson

मराठी cards