Docker Hub pull-through cache
Mirror Docker Hub and other public registries through ECR in your own account, so runners pull images over the VPC and never hit 429 rate limits.
CI jobs that docker pull public images eventually hit Docker Hub’s anonymous pull-rate limit (429 Too Many Requests / toomanyrequests). Because every runner in a stack shares one NAT egress IP, a busy organization trips that limit quickly.
RunsOn can mirror Docker Hub through an ECR pull-through cache ↗ in your own AWS account. The first pull of an image fetches it from Docker Hub into ECR; every later pull — across all your runners — is served from ECR over the VPC. That removes the rate limit and speeds up pulls.
Requirements#
The pull-through cache is configured through the Terraform / OpenTofu module and works on Fleet and Flex-on-Terraform stacks. It is not available on the CloudFormation install path. Linux only (it is a no-op on Windows).
Two things must be in place:
- An ECR pull-through cache rule for the upstream registry, created in your AWS account.
- The
ecr-pull-throughrunner extra, so the agent logs the runner into ECR (and, for Docker Hub, configures the daemon mirror) before your job starts.
1. Create the pull-through rule#
RunsOn references existing pull-through cache rules; it does not create them (creating one needs ecr:CreatePullThroughCacheRule, which the runner role intentionally does not hold). Create the regional rule once, outside the RunsOn module:
resource "aws_ecr_pull_through_cache_rule" "docker_hub" { ecr_repository_prefix = "ROOT" # special prefix that enables transparent mirroring upstream_registry_url = "registry-1.docker.io"}The ROOT prefix paired with registry-1.docker.io is what unlocks the transparent Docker Hub mirror below. Other upstreams (ECR Public, GHCR, Quay, registry.k8s.io, …) work too, but only Docker Hub is mirrored transparently — for the rest you reference images through the ECR path explicitly.
2. Reference the rule in the RunsOn stack#
Pass the rule to the Fleet or Flex module via ecr_pull_through_cache_rules:
module "runs_on" { source = "runs-on/runs-on/aws//modules/fleet" # ...
ecr_pull_through_cache_rules = { docker_hub = { ecr_repository_prefix = "ROOT" upstream_registry_url = "registry-1.docker.io" upstream_repository_prefix = "" } }}This grants runners the EcrPullThroughCacheAccess IAM policy (ecr:GetAuthorizationToken, BatchImportUpstreamImage, BatchGetImage, CreateRepository, …) so ECR can lazily create the cache repository and import the upstream image on first pull. See ecr_pull_through_cache_rules in the configuration reference.
3. Enable the extra on runners#
Add ecr-pull-through to the runner’s extras.
jobs: build: runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=ecr-pull-through steps: - uses: actions/checkout@v6 - run: docker pull node:22 # transparently served from your ECR mirror - run: docker build . # FROM docker.io/... images are mirrored too# Fleet: bake it into the runner definitionrunners = { linux-docker = { cpu = 8 ram = 16 family = ["c8i"] image = "ubuntu24-full-x64" extras = ["s3-cache", "ecr-pull-through"] }}For Docker Hub, no image references change. The agent writes a registry-mirrors entry into /etc/docker/daemon.json pointing at your ECR registry, so a plain docker pull node:22 or a FROM node:22 in a Dockerfile is served from the mirror automatically.
Environment variables#
When the extra is active, two variables are available to your job:
RUNS_ON_ECR_PULL_THROUGH_CACHE— the ECR registry host (e.g.123456789012.dkr.ecr.us-east-1.amazonaws.com). Use it to build an explicit mirror path for non–Docker Hub upstreams:${RUNS_ON_ECR_PULL_THROUGH_CACHE}/<ecr_repository_prefix>/<image>.RUNS_ON_ECR_PULL_THROUGH_CACHE_DOCKER_HUB_MIRROR— set totrueonly when the transparent Docker Hub mirror is configured, so a script can detect it.
Limitations#
- Anonymous upstream pulls. RunsOn authenticates the runner to your ECR, not to the upstream registry. The transparent mirror already removes Docker Hub’s anonymous limit because pulls come from ECR; to pull private upstream images or get authenticated Docker Hub limits, attach a credential to the pull-through rule itself (
CredentialArn) when you create it. - Docker daemon only. Transparency is implemented through the Docker daemon’s
registry-mirrors, so it applies todocker pulland Buildx builds using the default Docker driver. A non-Docker buildx driver or a containerd-only setup will not inherit the mirror. - No automatic expiry. Cached upstream images accumulate in the ECR cache repositories. Add your own ECR lifecycle policy ↗ if you want them pruned.
- Private networking. If runners launch in private subnets, they need a route to ECR (ECR + S3 endpoints, via NAT or interface VPC endpoints) the same as any other ECR access.