self-host →

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 instead of the public internet — no more 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:

  1. An ECR pull-through cache rule for the upstream registry, created in your AWS account.
  2. The ecr-pull-through runner 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 definition
runners = {
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 to true only 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 to docker pull and 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.