Guide · AWS ECR · Container Supply Chain

ECR Pull-Through Cache for MCP Servers — Docker Hub, GHCR, Upstream Registry Caching

ECR pull-through cache lets your ECS tasks and CI pipelines pull public images from Docker Hub, GHCR, Quay.io, and other registries through your own ECR endpoint — the image is fetched once from upstream and then cached in your registry. Three operational surprises: ECR automatically creates the destination repository on first pull, which requires the IAM principal making the pull to have ecr:CreateRepository on Resource: "*" — most ECS task execution roles lack this permission and silently fall back to direct upstream pull (or fail); cached images do not automatically expire — you must add lifecycle policies to the auto-created pull-through cache repositories or storage costs accumulate just like any ECR repository; and the pull URI format is <account>.dkr.ecr.<region>.amazonaws.com/<prefix>/<original-path>, so node:22-alpine from Docker Hub becomes 123456789.dkr.ecr.us-east-1.amazonaws.com/docker-hub/node:22-alpine — Dockerfiles and ECS task definitions must be updated to use the ECR URI.

TL;DR

Create a pull-through cache rule: aws ecr create-pull-through-cache-rule --ecr-repository-prefix docker-hub --upstream-registry-url registry-1.docker.io. Ensure the IAM principal pulling images has ecr:CreateRepository on * and ecr:BatchImportUpstreamImage. Update Dockerfiles to reference <account>.dkr.ecr.<region>.amazonaws.com/docker-hub/<image>:<tag>. Add lifecycle policies to the auto-created docker-hub/* repositories immediately after setup. For Docker Hub private images or to bypass rate limits, add a Secrets Manager secret with Docker Hub credentials and reference it in the cache rule.

Supported upstream registries

RegistryUpstream URLECR prefix (convention)Auth required
Docker Hubregistry-1.docker.iodocker-hubOptional (for private images or rate limit bypass)
Amazon ECR Publicpublic.ecr.awsecr-publicNo (public images)
Quay.ioquay.ioquayOptional (for private images)
GitHub Container Registryghcr.ioghcrOptional (for private packages)
Kubernetes registryregistry.k8s.iok8sNo
Azure Container Registry*.azurecr.ioacrYes (ACR credentials)
GitLab Container Registryregistry.gitlab.comgitlabOptional

Each upstream registry requires a separate pull-through cache rule. You can have up to 50 pull-through cache rules per registry (account + region). The ECR prefix is free-form — use a consistent naming convention across your organization.

Creating cache rules

## Create pull-through cache rule for Docker Hub (public images)
aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix docker-hub \
  --upstream-registry-url registry-1.docker.io \
  --region us-east-1

## Create rule for Amazon ECR Public
aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix ecr-public \
  --upstream-registry-url public.ecr.aws

## Create rule for GHCR (public packages)
aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix ghcr \
  --upstream-registry-url ghcr.io

## List existing rules
aws ecr describe-pull-through-cache-rules

## Delete a rule (does NOT delete the cached repositories — those remain in ECR)
aws ecr delete-pull-through-cache-rule \
  --ecr-repository-prefix docker-hub
// CDK: create pull-through cache rules
import * as ecr from "aws-cdk-lib/aws-ecr";

// CDK L1 (CfnPullThroughCacheRule) — no L2 construct exists yet
new ecr.CfnPullThroughCacheRule(this, "DockerHubCache", {
  ecrRepositoryPrefix: "docker-hub",
  upstreamRegistryUrl: "registry-1.docker.io",
  // credentialArn: optional — only for authenticated upstream access
});

new ecr.CfnPullThroughCacheRule(this, "GhcrCache", {
  ecrRepositoryPrefix: "ghcr",
  upstreamRegistryUrl: "ghcr.io",
});

IAM permissions: the CreateRepository gap

On the first pull of an upstream image, ECR automatically creates the destination repository (e.g., docker-hub/node). The IAM principal performing the pull must have permission to create repositories. This is the most common reason pull-through cache silently fails — the pull falls back to direct upstream without an explicit error.

// IAM policy for ECS task execution role to use pull-through cache
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRAuth",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "ECRPullThroughCache",
      "Effect": "Allow",
      "Action": [
        "ecr:CreateRepository",            // required: ECR creates repo on first cache miss
        "ecr:BatchImportUpstreamImage",    // required: fetches image from upstream registry
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "*"                      // CreateRepository and BatchImportUpstreamImage
                                           // must be on "*" — specific ARN isn't known at pull time
    }
  ]
}

The AmazonECSTaskExecutionRolePolicy managed policy does NOT include ecr:CreateRepository or ecr:BatchImportUpstreamImage. You must attach an inline policy or a custom managed policy with these permissions to any execution role that pulls images through a pull-through cache rule.

Pull URI format and Dockerfile updates

Images pulled through the cache are accessed via your private ECR endpoint. The path appends the upstream image path after the ECR prefix:

## Original Docker Hub image:
docker pull node:22-alpine

## Via ECR pull-through cache (Docker Hub):
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/docker-hub/library/node:22-alpine
#            ^account^      ^region^               ^prefix^ ^library = official images^

## Official Docker Hub images require the "library/" namespace:
## docker.io/node → docker-hub/library/node
## docker.io/nginx → docker-hub/library/nginx

## Non-library Docker Hub images (e.g., bitnami/postgresql):
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/docker-hub/bitnami/postgresql:15

## GHCR:
## Original: ghcr.io/owner/repo:tag
## Via cache: 123456789012.dkr.ecr.us-east-1.amazonaws.com/ghcr/owner/repo:tag

## ECR Public:
## Original: public.ecr.aws/amazonlinux/amazonlinux:2
## Via cache: 123456789012.dkr.ecr.us-east-1.amazonaws.com/ecr-public/amazonlinux/amazonlinux:2
# Dockerfile: update FROM to use ECR pull-through cache
# Before:
FROM node:22-alpine

# After:
ARG ECR_REGISTRY
FROM ${ECR_REGISTRY}/docker-hub/library/node:22-alpine

# Build with:
# docker build \
#   --build-arg ECR_REGISTRY=123456789012.dkr.ecr.us-east-1.amazonaws.com \
#   -t mcp-server:latest .

Docker Hub authentication and rate limit bypass

Docker Hub rate-limits anonymous pulls to 100 pulls per 6 hours per IP for unauthenticated users, and 200/6h per account for free accounts. A CI system building many containers on shared AWS infrastructure (CodeBuild, GitHub Actions) shares the same public IP and hits this limit quickly. ECR pull-through cache with Docker Hub credentials bypasses the rate limit entirely — all pulls to upstream are authenticated and billed against your Docker Hub account (up to 5,000/day on a $5/month paid plan).

## Step 1: Create Secrets Manager secret with Docker Hub credentials
aws secretsmanager create-secret \
  --name ecr-pullthroughcache/docker-hub \
  --description "Docker Hub credentials for ECR pull-through cache" \
  --secret-string '{"username":"your-dockerhub-username","accessToken":"dckr_pat_..."}'

## Note: use an Access Token (not your password) — create at hub.docker.com/settings/security
## The secret name must start with "ecr-pullthroughcache/" for ECR to access it

## Step 2: Create pull-through cache rule with credential ARN
aws ecr create-pull-through-cache-rule \
  --ecr-repository-prefix docker-hub \
  --upstream-registry-url registry-1.docker.io \
  --credential-arn arn:aws:secretsmanager:us-east-1:123456789012:secret:ecr-pullthroughcache/docker-hub-AbCdEf
// IAM policy to allow ECR to read the Secrets Manager secret
// ECR needs this to authenticate to Docker Hub on your behalf
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRPullThroughCacheCredentials",
      "Effect": "Allow",
      "Principal": { "Service": "ecr.amazonaws.com" },
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:ecr-pullthroughcache/docker-hub-*",
      "Condition": {
        "StringEquals": { "aws:sourceAccount": "123456789012" }
      }
    }
  ]
}
// Attach this as a resource policy on the Secrets Manager secret

Cache TTL, storage, and lifecycle policies

ECR pull-through cache does NOT have a built-in TTL or auto-expiry. Once an image is cached, it remains in your ECR repository indefinitely until you delete it manually or via a lifecycle policy. The cached repository is a normal ECR repository — standard ECR storage pricing ($0.10/GB-month) applies.

## Add lifecycle policy to auto-created pull-through cache repositories
## (run this after the first pull creates the repository)
aws ecr put-lifecycle-policy \
  --repository-name docker-hub/library/node \
  --lifecycle-policy-text '{
    "rules": [
      {
        "rulePriority": 1,
        "description": "Keep only the 5 most recently pulled tags",
        "selection": {
          "tagStatus": "any",
          "countType": "imageCountMoreThan",
          "countNumber": 5
        },
        "action": { "type": "expire" }
      }
    ]
  }'

## For bulk lifecycle policy application to all pull-through cache repos,
## use a script that lists repos by prefix and applies the same policy:
for repo in $(aws ecr describe-repositories \
  --query 'repositories[?starts_with(repositoryName, `docker-hub/`)].repositoryName' \
  --output text); do
  aws ecr put-lifecycle-policy \
    --repository-name "$repo" \
    --lifecycle-policy-text file://cache-lifecycle.json
done

Cache refresh behavior: on a cache hit (image exists in ECR), the pull completes immediately from ECR without going to upstream. On a cache miss (image not in ECR or tag not cached), ECR fetches from upstream, stores it, and serves it — this takes a few extra seconds on first pull. Digest-based pulls (sha256:...) bypass the cache and always pull from upstream if the digest doesn't exist in ECR.

AliveMCP: caching probe worker base images

AliveMCP's probe worker Dockerfiles use node:22-alpine as the base image. Without pull-through cache, the AWS CodeBuild instances building probe worker images hit Docker Hub rate limits — especially during high-frequency builds triggered by dependency updates. The pull-through cache rule for Docker Hub (with AliveMCP's paid Docker Hub account credentials) routes all base image pulls through ECR, eliminating rate limit errors and reducing build times by ~20 seconds (ECR pull within the same VPC is faster than Docker Hub over the public internet). The docker-hub/library/node repository is kept to 5 images by lifecycle policy, keeping cache storage under 500 MB.

Common failure modes

SymptomCauseFix
Pull falls back to Docker Hub directly instead of using ECR cacheIAM principal lacks ecr:CreateRepository or ecr:BatchImportUpstreamImageAdd these permissions to the execution role's policy; check CloudTrail for AccessDenied on these actions
docker pull fails: "no such image" for official Docker Hub imagesOfficial Docker Hub images require the library/ namespace prefix in the ECR pathUse docker-hub/library/node:22-alpine not docker-hub/node:22-alpine
ECR storage grows unboundedly for pull-through cache reposNo lifecycle policy on the auto-created cache repositoriesApply lifecycle policies to all docker-hub/* repositories; use a script to apply policies in bulk
Secrets Manager credential not found by ECRSecret name must start with ecr-pullthroughcache/ for ECR to access itRename the secret to have the required prefix; update the ARN in the cache rule
Pull-through cache rule created but images still rate-limited by Docker HubCache rule created but ECR not configured to use Docker Hub credentials — pulls are anonymousEnsure credentialArn is set in the cache rule; verify the Secrets Manager resource policy grants ECR read access
Cannot delete pull-through cache rule: ValidationExceptionCached repositories with images exist — ECR does not auto-delete them when the rule is removedDelete the cache rule first; then manually clean up the remaining docker-hub/* repositories