Skip to content
Latchkey LogoLatchkey home

A Docker registry mirror for GitHub Actions

A Docker pull-through cache is a registry mirror that answers docker.io pulls from a copy it keeps, so the pull stops counting against Docker Hub's limit. On a GitHub Actions runner it buys very little time, measured at 4.1 seconds against 4.7 for a 160 MB image, and it buys back the rate limit, which is the reason to run one in CI.

Bar chart of pull times for one image: direct, through a mirror, and through a warm cache
Measured on a Latchkey latchkey-small runner on 2026-09-20, pulling postgres:16 with the local image store emptied before every pull.
Diagram of a pull-through cache in front of Docker Hub and what it does not cover
A mirror is a Docker Hub feature. Images from other registries walk straight past it, which is the single most common reason a configured mirror looks broken.

Most write-ups about registry mirrors lead with speed, and then quote no numbers. This page has the numbers from one runner, measured the day it was written, and they do not support the speed claim. What the measurement does support is the other argument, which is usually made in passing: a pull that an on-network cache answers never reaches Docker Hub, so it cannot be refused by the Docker Hub pull rate limit.

It also covers the part that wastes the most time in practice, which is a mirror that is configured and not used. The daemon falls back to the upstream registry silently when the mirror cannot answer, so a typo, an unreachable host or a forgotten reload all look exactly like no mirror at all.

What a registry mirror actually is

The registry-mirrors key in daemon.json tells the Docker daemon to try a different host first for images that live on Docker Hub. The mirror is itself a registry running in proxy mode: on a miss it fetches from Hub, stores what it fetched, and serves the copy to everyone who asks next.

Two properties of that arrangement cause almost all of the confusion. It applies to Docker Hub only, so an image referenced from GHCR, ECR, Quay or a private host ignores it entirely. And it is a fallback rather than a requirement: if the mirror does not answer, the daemon goes to the upstream registry and the pull succeeds, which means a broken mirror produces no error anywhere.

Measured on a runner

Each row is a pull of postgres:16, which is 160,299,139 bytes on disk across 14 layers, with the local image store emptied first so nothing is reused. Measured on a Latchkey latchkey-small runner on 2026-09-20. The last two rows use a registry:2 container in proxy mode on the same machine; its store held 158.9 MB after the first pull.

Path the pull tookTimeCounts against Docker Hub
Straight to registry-1.docker.io4.9 s, then 4.6 sYes
Through the pull-through mirror the runner ships with4.7 s, then 4.7 sNo
Through a local registry:2 cache, cache empty5.8 sYes, once
Through the same local cache, cache warm4.1 s, then 4.1 sNo

Run one

The registry image runs as a pull-through cache with one environment variable. Give it a volume, because a cache that starts empty on every job is the cold row in the table above and nothing else.

Point the daemon at it and reload. On a machine you control that is a file and a restart; in a job, the restart costs a few seconds and has to happen before anything pulls.

Terminal
docker run -d --name regcache --restart always -p 5000:5000 \
  -v /var/lib/regcache:/var/lib/registry \
  -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
  registry:2

# /etc/docker/daemon.json
{ "registry-mirrors": ["http://mirror.internal.example.com:5000"] }

sudo systemctl restart docker

Prove it is being used

Because a failed mirror is silent, the only way to know is to look. docker info reports the configured mirrors, which tells you the daemon read the file; it does not tell you the mirror answered. The mirror's own request log does.

Check both, once, when you set it up, and again the first time somebody reports the rate limit is back. Those two commands take ten seconds and settle an argument that otherwise takes an afternoon.

Terminal
docker info --format "{{.RegistryConfig.Mirrors}}"
docker pull postgres:16
docker logs regcache --since 1m | tail -5

Why a configured mirror looks like it is not working

The image is not on Docker Hub. This is the most common one by a distance. registry-mirrors covers docker.io and nothing else, so a workflow that moved its base images to GHCR last year has a mirror that correctly does nothing.

The daemon was never reloaded. Editing daemon.json changes a file; the running daemon keeps the configuration it started with until it is restarted, and in a job that means the edit has to come before the first pull rather than after it.

The mirror is unreachable and the fallback hid it. A wrong port, an http URL where the daemon wanted https, or a cache that is simply down all end with the pull succeeding from upstream, which is why nobody notices until the limit is hit again.

The pull is authenticated to a private repository. A pull-through cache handles public Hub images; a private one needs credentials the cache does not have, and the daemon goes direct.

When a mirror is the wrong tool

If you pull a small, stable set of base images, copying them into a registry you already run is simpler and covers more ground: it works for every registry rather than for Hub, it needs no daemon configuration, and there is no fallback to hide a mistake. The cost is that you own the copies and their updates.

If your problem is build time rather than pull count, a layer cache is the tool and a registry mirror is not. The measurement above is the evidence: on a runner with good egress, the cache saved less than a second on a 160 MB image.

A Latchkey runner ships with a pull-through cache already configured in front of Docker Hub, which is the row in the table that neither counts against the limit nor asks you to run anything. The rate-limit failure itself, and what the engine does when it fires, are on the Docker Hub pull rate limit page.

Key takeaways

  • A registry mirror is for the pull limit, not for speed: 4.1 s against 4.7 s on the runner we measured.
  • registry-mirrors applies to Docker Hub images only, and everything else walks past it.
  • A mirror that cannot answer is silent, because the daemon falls back to the upstream registry.
  • Give the cache a volume, or every job pays the cold-cache second and caches nothing.

Frequently asked questions

Why is my Docker registry mirror not being used?
Most often because the image is not a Docker Hub image, and registry-mirrors covers docker.io only. After that: the daemon was not restarted after daemon.json changed, or the mirror host is unreachable and the daemon quietly fell back to the upstream registry. Read the mirror's own request log rather than docker info to tell those apart.
Does registry-mirrors work for images that are not on Docker Hub?
No. It is a Docker Hub pull-through feature, so GHCR, ECR, Quay and private registries ignore it. To cache those, run a registry with a proxy configuration per upstream, or copy the images you need into one registry you control and reference them from there.
Do I need to restart the Docker daemon after editing daemon.json?
Yes. The daemon reads that file at start, so the change has no effect until it reloads. In a workflow that means editing the file and restarting before the first pull, which costs a few seconds; on a machine you own, it is a one-time step at provisioning.
Is a pull-through cache faster than pulling from Docker Hub?
Barely, on a runner with good egress. We measured 4.1 seconds through a warm local cache against 4.7 through the mirror path and 4.9 direct, for a 160 MB image. On a slow or metered link the gap widens, but the reason to run one is that the pull no longer counts against the rate limit.

Related guides

References

A mirror buys back the rate limit, not the minutes. Latchkey keeps a Docker layer cache between jobs. Start free → 30-day trial · No credit card