Docker "registry-mirrors" Ignored or Unreachable - Pull-Through Cache Not Working in CI
A registry-mirrors entry in daemon.json sets a pull-through cache for Docker Hub. It is commonly ineffective: it only applies to Docker Hub images, only to anonymous pulls in some setups, and a typo or an unreachable mirror makes pulls quietly fall back to the upstream (and its rate limits).
What this error means
Pulls still hit Docker Hub (and its rate limit) despite a configured mirror, or pulls of non-Hub images ignore the mirror entirely. A docker info shows the mirror, but the cache is not being used as expected.
# daemon.json had: { "registry-mirrors": ["https://mirror.example.com"] }
# yet pulls still fail with:
toomanyrequests: You have reached your pull rate limit
# (the mirror was unreachable, or the image was not a Docker Hub image)Diagnose it: separate auth from naming from rate limits
Registry errors look alike and have unrelated causes. Work out which of the three you have before changing credentials, because a malformed image reference produces an error that reads like an authentication failure.
# 1. is the reference even valid? (lowercase, no spaces, valid tag)
docker image inspect "$IMAGE" 2>&1 | head -2
# 2. are you authenticated to the right registry?
cat ~/.docker/config.json | grep -o '"[^"]*\.[^"]*"' | head
# 3. are you rate limited? (Docker Hub anonymous pulls)
curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimit-preview/test:pull" \
| grep -o '"token"' >/dev/null && echo "token ok"Common causes
Mirrors only apply to Docker Hub images
registry-mirrors is a pull-through cache for docker.io only. Images from GHCR, ECR, or other registries ignore the mirror entirely.
The mirror is unreachable or misconfigured
A typo, a wrong scheme/port, or a down mirror makes the daemon fall back to the upstream registry, so pulls still hit Hub and its limits.
Daemon not reloaded after editing daemon.json
Changes to registry-mirrors only take effect after a daemon restart. Without it, the running daemon still uses the old (or no) mirror.
How to fix it
Configure a reachable Hub mirror and restart
Point at a working pull-through cache for Docker Hub and reload the daemon.
# /etc/docker/daemon.json
{ "registry-mirrors": ["https://mirror.example.com"] }
sudo systemctl restart docker
docker info | grep -A2 'Registry Mirrors'Mirror non-Hub registries differently
- For GHCR/ECR/etc., pull through your cloud provider’s registry cache or copy images into your own registry.
- Confirm the mirror host is reachable from the runner network.
- Authenticate to Docker Hub as well - a mirror does not remove the auth-based rate limits.
Authenticate in the job, not in the image
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# GHCR needs this on the job or the push is rejected as unauthorised
permissions:
contents: read
packages: writeHow to prevent it
- Remember
registry-mirrorsonly caches Docker Hub images. - Verify the mirror is reachable and restart the daemon after config changes.
- Use provider-specific caches for non-Hub registries.