Your CompanyPlatform Engineering
mirror online

ghcr-mirror / pull-through cache

This host proxies and caches image pulls from ghcr.io. Point Docker or containerd here to speed up pulls of your org's published images and cut outbound rate-limit hits. Replace mirror.example.com below with this server's actual hostname.

01 — AUTHENTICATE

Log in with a PAT

Most ghcr.io images require auth even for pulls. Generate a personal access token with the read:packages scope, then log in — this credential works whether you pull directly from GitHub or through the mirror.

shell
export CR_PAT=YOUR_TOKEN
echo $CR_PAT | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
02 — LINUX / DOCKER ENGINE

Edit daemon.json

Add this mirror as a registry-mirror in the Docker daemon config, then restart the daemon.

/etc/docker/daemon.json
{
  "registry-mirrors": ["https://mirror.example.com"]
}
shell
# apply the config
sudo systemctl daemon-reexec
sudo systemctl restart docker

Since ghcr.io images are always pulled with the full ghcr.io/... reference, the containerd hosts.toml approach below is the more reliable per-registry mirror on most setups.

03 — DOCKER DESKTOP

macOS / Windows

Open Settings → Docker Engine, merge the same key into the JSON editor there, then click Apply & Restart.

Docker Desktop → Settings → Docker Engine
{
  "builder": { "gc": { "enabled": true } },
  "registry-mirrors": ["https://mirror.example.com"]
}
04 — CONTAINERD / KUBERNETES

hosts.toml, scoped to ghcr.io

For containerd nodes — k3s, k8s, bare containerd — configure a mirror specifically for the GHCR namespace:

/etc/containerd/certs.d/ghcr.io/hosts.toml
server = "https://ghcr.io"

[host."https://mirror.example.com"]
  capabilities = ["pull", "resolve"]

Restart containerd afterward: sudo systemctl restart containerd. For private images, put an imagePullSecret referencing your GitHub PAT on the pod spec as usual — auth still flows through even when the pull is served by the mirror.

05 — VERIFY

Confirm the daemon picked it up

shell
docker info --format '{{ .RegistryConfig.Mirrors }}'

# pull a ghcr.io image and check it came from the mirror
docker pull ghcr.io/your-org/your-image:latest
curl -s https://mirror.example.com/v2/ -o /dev/null -w "%{http_code}\n"
CheckExpected result
docker infoMirror URL listed under Registry Mirrors
GET /v2/200 or 401 — mirror is reachable
First pull of an imageSlower — cache miss, fetched from upstream and stored
Repeat pull of same imageFast — served from local cache
06 — NOTES
Pull-through, not a full copy. This mirror caches images as they're requested — it doesn't proactively sync your org's entire GHCR namespace. The first pull of any given image/tag is still fetched from ghcr.io.

docker push still goes straight to ghcr.io — this mirror only accelerates pulls. Image visibility (public vs. private) and package permissions are enforced by GitHub regardless of which path the pull takes. If this mirror is served over plain HTTP internally, add it to insecure-registries instead of registry-mirrors, or terminate TLS in front of it.