Local Docker Registry as a Pull-Through Cache: Speeding Up CI Builds
AI generated
FROM
RUN
Docker · CI/CD · Registry
A Local Registry as a Pull-Through Cache
Bypass Docker Hub rate limits and drastically cut CI pull times

Teams that regularly hit Docker Hub rate limits in CI pipelines can cut pull times and avoid failures with a local registry acting as a pull-through cache, without manually mirroring images.

17 min read Pull-Through Cache Rate Limits CI/CD

1. The Problem: Docker Hub Rate Limits in CI Pipelines

Docker Hub caps anonymous pulls at 100 requests per six hours and authenticated pulls at 200 requests in the same window, sometimes less depending on the account type. In a CI environment with several parallel pipelines each pulling base images, build tools, and service containers, that limit is reached quickly, especially when many runners sit behind the same public IP address and effectively share the limit.

The result is builds failing with a 'toomanyrequests' error that seems random and costs a lot of debugging time, because the cause is infrastructure, not code. A pull-through cache solves this structurally: instead of hitting Docker Hub for every single pull, the cache registry serves repeated requests from local storage and reduces actual Docker Hub traffic to a minimum.

2. How a Pull-Through Cache Works

Technically, a pull-through cache is a regular Docker registry (the official registry:2 image) running in proxy mode. On the first pull of an image, the cache registry forwards the request to Docker Hub, stores the result locally, and serves it. Every subsequent pull of the same image and tag is served directly from local storage without contacting Docker Hub again, as long as the cache entry is still valid.

For clients, little changes: instead of docker.io, the own registry is used as a prefix in the image name, or the Docker daemon is configured via registry-mirrors so it automatically routes through the local registry. From the pipeline's perspective the behavior looks identical, except repeated pulls complete in milliseconds instead of seconds and no external rate limits apply anymore.

3. Setting Up a Registry as a Pull-Through Cache

Setup happens through a configuration file for the registry, where a proxy block with remoteurl points to https://registry-1.docker.io. The registry:2 image reads this configuration on startup and afterwards behaves transparently as a cache in front of Docker Hub. It is important to run one cache registry per upstream: anyone who also wants to mirror GHCR or Quay needs a separate registry instance with its own port for each.

Persistent storage for the registry data is mandatory, otherwise the cache is lost on every restart and the first wave of builds after a deploy hits Docker Hub unthrottled again. A dedicated volume for /var/lib/registry is enough for that and should be checked regularly for storage usage, since the cache keeps growing continuously without cleanup.


# docker-compose.yml
services:
  registry-cache:
    image: registry:2
    ports:
      - "5000:5000"
    environment:
      REGISTRY_PROXY_REMOTEURL: "https://registry-1.docker.io"
      REGISTRY_PROXY_TTL: "168h"
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
    volumes:
      - registry-cache-data:/var/lib/registry
    restart: unless-stopped

volumes:
  registry-cache-data:

4. Pointing CI Runners at the Local Registry

For CI jobs to actually use the cache, the runners' Docker daemon must be configured via registry-mirrors in daemon.json. This entry routes all pulls from docker.io through the given mirror URL automatically, without any need to touch Dockerfiles or CI scripts. That is the decisive advantage over manually retagging images: existing pipelines benefit immediately without code changes.

For self-hosted runners (GitLab Runner, Jenkins agents, GitHub Actions runners on own infrastructure) this is straightforward, since the daemon can be configured directly. On hosted CI providers without access to the Docker daemon, the approach is limited, and the only alternative is often explicitly renaming the image field in the pipeline definition.


# /etc/docker/daemon.json on the CI runner
{
  "registry-mirrors": ["http://registry-cache.internal:5000"]
}

# Restart the Docker daemon afterwards
sudo systemctl restart docker

# Test: the pull should now go through the cache
docker pull node:20-alpine
docker logs registry-cache 2>&1 | grep "GET /v2/node""

5. Storing Docker Hub Credentials for Higher Limits

Without credentials, the cache registry acts as an anonymous client against Docker Hub and is itself subject to the anonymous rate limit of 100 pulls per six hours, just centralized for all runners instead of per runner. That is often enough for small teams, but larger CI fleets with many daily builds benefit from authenticated access, which raises the limit to 200 pulls or further with a paid Docker Hub plan.

Credentials are stored via REGISTRY_PROXY_USERNAME and REGISTRY_PROXY_PASSWORD as environment variables of the cache registry, ideally as a secret and not as plaintext in the compose file. A dedicated Docker Hub user with read-only permissions is a good idea, so a compromised cache server does not immediately have write access to production repositories.


services:
  registry-cache:
    image: registry:2
    environment:
      REGISTRY_PROXY_REMOTEURL: "https://registry-1.docker.io"
      REGISTRY_PROXY_USERNAME: "${DOCKERHUB_CACHE_USER}"
      REGISTRY_PROXY_PASSWORD: "${DOCKERHUB_CACHE_TOKEN}"
    env_file:
      - .env.registry-cache

6. Cache Invalidation and Tag Updates

The critical point of any pull-through cache is how it handles mutable tags like latest or 20-alpine, which get rebuilt regularly upstream. The registry checks the tag's digest via a HEAD request on every pull attempt and automatically re-fetches when it differs. As long as the network to Docker Hub is reachable, clients always get the current state, even if the cache still holds an older layer.

REGISTRY_PROXY_TTL controls how long a cache entry is kept at most before it is considered expired regardless of the digest check and fully re-pulled. For CI environments, a TTL of roughly one week is a good compromise: short enough to get security updates in base images promptly, long enough to keep the hit rate high and avoid putting unnecessary load on Docker Hub.

7. Storage Management and Garbage Collection

A pull-through cache grows continuously because every newly requested image is stored permanently, even if it is never pulled again. Without cleanup, the volume eventually fills up completely, which in the worst case means the registry can no longer accept new layers and builds fail, even though the original rate-limit problem was solved long ago.

The registry ships with a built-in garbage collection command that removes unused blobs no longer referenced by any manifest. It must be run while the proxy is in a read-only window, so no new layers are written during the run, most easily as a scheduled maintenance job outside the CI pipelines' main working hours.


# Run garbage collection inside the registry container
docker exec registry-cache \
  registry garbage-collect /etc/docker/registry/config.yml

# Check current storage usage of the cache volume
docker system df -v | grep registry-cache-data

8. Monitoring the Cache Hit Rate

Whether the effort of running a pull-through cache pays off only becomes clear through monitoring: the registry logs every incoming request including the path, so hits and actual upstream requests can be distinguished from the logs. A simple start is parsing log lines for pull requests and checking how many of them were actually forwarded to registry-1.docker.io.

For ongoing monitoring, exporting registry metrics via the built-in Prometheus endpoint is worthwhile, which provides request counters per status code among other things. That makes it possible to build a simple Grafana dashboard with cache hit rate and remaining Docker Hub quota, which shows early when a team's usage pattern changes and the limit gets tight again despite the cache.

9. Limits of the Approach and Alternatives

A single pull-through cache is a single point of failure: if the registry goes down, the entire CI operation falls back to direct Docker Hub pulls, in the worst case exactly when the rate limit is already tight. For production environments a redundant setup with at least two registry instances behind a load balancer, sharing the same storage backend such as S3-compatible object storage instead of a local volume, is recommended.

Anyone who needs more than plain caching, such as RBAC, vulnerability scanning, or multi-registry replication, eventually ends up at full-fledged solutions like Harbor or Sonatype Nexus Repository. Both also support pull-through caching but come with considerably more operational overhead. For the pure purpose of bypassing rate limits in CI, the lean registry:2 solution is usually the more pragmatic starting point.

Solution Setup effort Extra features Recommendation
registry:2 proxy mode Low, one container Caching only Quickly solving CI rate limits
Harbor Medium, several components RBAC, scanning, replication Team-wide registry platform
Sonatype Nexus Repository Medium to high Multi-format, proxy for many ecosystems Existing Nexus infrastructure
No cache, direct pulls None None Only for very few daily builds

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

Pull-Through Cache: The Essentials at a Glance

Problem

Docker Hub rate limits block CI pipelines with many parallel pulls.

Solution

registry:2 in proxy mode as a local pull-through cache in front of Docker Hub.

Integration

registry-mirrors in daemon.json, no changes to Dockerfiles needed.

Operations

TTL for freshness, garbage collection against unbounded growth.

11. FAQ: Pull-Through Cache: The Essentials at a Glance

1What is a pull-through cache in Docker?
A pull-through cache is a separate registry instance that forwards requests to an upstream registry like Docker Hub, stores the result locally, and serves repeated requests from its own storage without contacting the upstream again.
2What are the current Docker Hub rate limits?
Anonymous users may perform 100 pulls per six hours per IP address, authenticated free accounts 200 pulls in the same window. Paid plans raise the limit considerably; Docker changes the exact numbers occasionally, so checking current Docker Hub documentation is worthwhile.
3Do Dockerfiles need to be changed for the cache?
No, if the CI runners' Docker daemon is configured via registry-mirrors, all pulls from docker.io automatically go through the cache without changing image names in Dockerfiles or pipeline definitions.
4What happens when an image is updated upstream?
The registry checks the current digest upstream via a HEAD request on every pull and automatically re-fetches on mismatch, as long as the connection to upstream exists. That way clients always get the current state of mutable tags despite caching.
5How is cache storage limited?
Via REGISTRY_PROXY_TTL a cache entry expires after a defined time, and the built-in garbage collection command regularly removes blobs no longer referenced by any manifest.
6Can I cache multiple upstream registries at once?
Yes, but each registry instance can only serve one upstream in proxy mode. To cache Docker Hub, GHCR, and Quay in parallel, three separate cache instances with their own ports are needed.
7Is authenticated access to the cache worth it?
Small teams often get by with anonymous access at 100 pulls per six hours, centralized for all runners. Larger CI fleets with many daily builds benefit from an authenticated Docker Hub user raising the limit considerably.
8Is the pull-through cache a replacement for an own registry?
No, the cache only mirrors images that already exist in an upstream. Self-built images still need a separate, writable registry, which can also be operated independently of the cache.
9How do I ensure high availability for the cache?
Multiple registry instances behind a load balancer sharing a common storage backend such as S3-compatible object storage avoid a single point of failure compared to a single instance with a local volume.
10When is Harbor worth it over the simple registry:2 solution?
As soon as RBAC, vulnerability scanning, or multi-registry replication are also needed, Harbor is the better fit. For the pure purpose of bypassing CI rate limits, the lean proxy configuration of registry:2 is usually sufficient.