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.
Table of Contents
- 1. The Problem: Docker Hub Rate Limits in CI Pipelines
- 2. How a Pull-Through Cache Works
- 3. Setting Up a Registry as a Pull-Through Cache
- 4. Pointing CI Runners at the Local Registry
- 5. Storing Docker Hub Credentials for Higher Limits
- 6. Cache Invalidation and Tag Updates
- 7. Storage Management and Garbage Collection
- 8. Monitoring the Cache Hit Rate
- 9. Limits of the Approach and Alternatives
- 10. Summary
- 11. FAQ
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.