|
| 1 | +--- |
| 2 | +date: '2026-03-12' |
| 3 | +title: Mirroring Docker Hardened Images to ECR with regclient |
| 4 | +description: How to work around ECR's lack of pull-through cache support for dhi.io by using regsync to automatically mirror Docker Hardened Images into your own ECR repositories. |
| 5 | +--- |
| 6 | +## Mirroring Docker Hardened Images to ECR with regclient |
| 7 | + |
| 8 | +I wrote about [switching to Docker Hardened Images](/posts/2025-12-27-switching-to-docker-hardened-images) on my personal Kubernetes cluster a few months ago. At work, we've been adopting them too. DHI images are minimal, distroless at runtime, and come with SBOMs and attestations out of the box. Now that they're free for everyone, there's really no reason not to use them. |
| 9 | + |
| 10 | +There's one problem though: if you're running on AWS and using ECR pull-through cache to avoid Docker Hub rate limits and keep your image pulls fast and local, you're out of luck. ECR's pull-through cache [doesn't support `dhi.io`](https://github.com/aws/containers-roadmap/issues/2727) as an upstream registry. It supports Docker Hub, GitHub Container Registry, Quay, and a handful of others, but not the DHI registry. There's an open feature request for it, but who knows when (or if) AWS will add it. |
| 11 | + |
| 12 | +So we built a workaround: use [regclient](https://regclient.org/)'s `regsync` tool to mirror the images ourselves on a schedule. |
| 13 | + |
| 14 | +### What is regsync? |
| 15 | + |
| 16 | +[regsync](https://regclient.org/usage/regsync/) is part of the regclient project -- a set of tools for working with container registries. regsync specifically handles mirroring images between registries. You give it a YAML config that defines source and target registries, credentials, and which repositories to sync, and it copies everything over. It supports tag filtering, multi-arch manifests, and a `fastCopy` mode that copies blobs directly between registries without pulling them to the local machine first. That last part is important -- it means the GitHub Actions runner doesn't need to have enough disk space to hold every image layer. |
| 17 | + |
| 18 | +### The GitHub Actions Workflow |
| 19 | + |
| 20 | +Here's the workflow we're running. It triggers on pushes to main (so config changes take effect immediately), on an hourly schedule (to pick up new upstream tags), and manually via `workflow_dispatch` for when you just want to force a sync: |
| 21 | + |
| 22 | +```yaml |
| 23 | +name: Mirror DHI Images to ECR |
| 24 | + |
| 25 | +on: |
| 26 | + push: |
| 27 | + branches: [main] |
| 28 | + schedule: |
| 29 | + - cron: "0 * * * *" |
| 30 | + workflow_dispatch: |
| 31 | + |
| 32 | +permissions: |
| 33 | + id-token: write |
| 34 | + contents: read |
| 35 | + |
| 36 | +env: |
| 37 | + AWS_REGION: us-east-1 |
| 38 | + AWS_ACCOUNT_ID: "123456789012" |
| 39 | + |
| 40 | +jobs: |
| 41 | + mirror: |
| 42 | + runs-on: ubuntu-24.04-arm |
| 43 | + steps: |
| 44 | + - uses: actions/checkout@v6 |
| 45 | + |
| 46 | + - name: Configure AWS credentials |
| 47 | + uses: aws-actions/configure-aws-credentials@v6 |
| 48 | + with: |
| 49 | + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/GitHubActionsRole |
| 50 | + aws-region: ${{ env.AWS_REGION }} |
| 51 | + |
| 52 | + - name: Get ECR token |
| 53 | + id: ecr-token |
| 54 | + run: echo "token=$(aws ecr get-login-password --region ${{ env.AWS_REGION }})" >> "$GITHUB_OUTPUT" |
| 55 | + |
| 56 | + - name: Install regclient |
| 57 | + run: | |
| 58 | + curl -sL https://github.com/regclient/regclient/releases/latest/download/regctl-linux-arm64 -o /usr/local/bin/regctl && chmod +x /usr/local/bin/regctl |
| 59 | + curl -sL https://github.com/regclient/regclient/releases/latest/download/regsync-linux-arm64 -o /usr/local/bin/regsync && chmod +x /usr/local/bin/regsync |
| 60 | +
|
| 61 | + - name: Sync images |
| 62 | + env: |
| 63 | + ECR_REGISTRY: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com |
| 64 | + ECR_TOKEN: ${{ steps.ecr-token.outputs.token }} |
| 65 | + DHI_USER: ${{ secrets.DOCKERHUB_USERNAME }} |
| 66 | + DHI_PASS: ${{ secrets.DOCKERHUB_TOKEN }} |
| 67 | + run: regsync once -c regsync.yaml -v info |
| 68 | +``` |
| 69 | +
|
| 70 | +A few things to note: |
| 71 | +
|
| 72 | +- We're using OIDC authentication (`id-token: write`) to assume an IAM role rather than storing long-lived AWS credentials as secrets. This is the right way to do AWS auth from GitHub Actions. |
| 73 | +- The runner is `ubuntu-24.04-arm` because ARM runners are slightly cheaper than x86 on GitHub Actions. regsync copies manifests and blobs directly between registries, so it syncs multi-arch images (including x86) regardless of what architecture the runner itself is. |
| 74 | +- DHI uses your Docker Hub credentials for authentication. The images are free to use, but `dhi.io` doesn't allow unauthenticated pulls -- it's a separate registry that authenticates against Docker Hub. |
| 75 | +- `regsync once` runs a single sync pass and exits, which is what you want in CI. There's also a `server` mode that runs continuously, but that's more suited for a long-running container. |
| 76 | + |
| 77 | +### The regsync Configuration |
| 78 | + |
| 79 | +The `regsync.yaml` file defines where to pull from, where to push to, and what to sync: |
| 80 | + |
| 81 | +```yaml |
| 82 | +version: 1 |
| 83 | +
|
| 84 | +defaults: |
| 85 | + skipDockerConfig: true |
| 86 | +
|
| 87 | +creds: |
| 88 | + - registry: dhi.io |
| 89 | + user: "{{ env \"DHI_USER\" }}" |
| 90 | + pass: "{{ env \"DHI_PASS\" }}" |
| 91 | + repoAuth: true |
| 92 | + - registry: "{{ env \"ECR_REGISTRY\" }}" |
| 93 | + user: AWS |
| 94 | + pass: "{{ env \"ECR_TOKEN\" }}" |
| 95 | +
|
| 96 | +x-deny-compliance: &deny-compliance |
| 97 | + tags: |
| 98 | + deny: |
| 99 | + - ".*-fips.*" |
| 100 | + - ".*-sfw.*" |
| 101 | +
|
| 102 | +sync: |
| 103 | + - source: dhi.io/node |
| 104 | + target: "{{ env \"ECR_REGISTRY\" }}/dhi-io/node" |
| 105 | + type: repository |
| 106 | + fastCopy: true |
| 107 | + <<: *deny-compliance |
| 108 | +
|
| 109 | + - source: dhi.io/python |
| 110 | + target: "{{ env \"ECR_REGISTRY\" }}/dhi-io/python" |
| 111 | + type: repository |
| 112 | + fastCopy: true |
| 113 | + <<: *deny-compliance |
| 114 | +``` |
| 115 | + |
| 116 | +The `x-deny-compliance` YAML anchor is worth calling out. DHI publishes FIPS and SFW (secure frameworks) variants of their images with tags like `3.12-fips` or `3.12-sfw`. These compliance-specific variants are paywalled, and we don't need them for our use case, so we exclude them with a tag deny list. The YAML anchor lets you apply the same filter to every sync entry without repeating yourself. |
| 117 | + |
| 118 | +### Adding More Images |
| 119 | + |
| 120 | +To mirror additional DHI images, just add more entries to the `sync` list: |
| 121 | + |
| 122 | +```yaml |
| 123 | + - source: dhi.io/nginx |
| 124 | + target: "{{ env \"ECR_REGISTRY\" }}/dhi-io/nginx" |
| 125 | + type: repository |
| 126 | + fastCopy: true |
| 127 | + <<: *deny-compliance |
| 128 | +``` |
| 129 | + |
| 130 | +You'll also need to make sure the ECR repository exists before the first sync. ECR recently added support for [creating repositories on push](https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-ecr-creating-repositories-on-push/), but if you're not using that, you can create them manually or manage them in Terraform like we do. |
| 131 | + |
| 132 | +### Why Not Just Pull Directly from dhi.io? |
| 133 | + |
| 134 | +You could, and for small-scale usage it works fine. But there are a few reasons to mirror: |
| 135 | + |
| 136 | +- **Rate limits.** Docker Hub (and by extension dhi.io) has pull rate limits. If you're running a large cluster with frequent deployments or node scaling events, you'll hit them. ECR has no pull rate limits within the same region. |
| 137 | +- **Latency.** Pulling from ECR in the same region as your EKS cluster is significantly faster than pulling from an external registry. |
| 138 | +- **Availability.** If dhi.io or Docker Hub has an outage, your deployments still work because the images are already in ECR. We've experienced login failures and 503 errors during image pulls from dhi.io -- not frequently, but enough that you don't want to depend on it for production deployments. |
| 139 | +- **Compliance.** Some organizations require all container images to come from an internal registry for audit and scanning purposes. |
| 140 | + |
| 141 | +### Bottom Line |
| 142 | + |
| 143 | +Until AWS adds `dhi.io` as a supported pull-through cache upstream, regsync is a clean workaround. The setup takes maybe 30 minutes, runs on a GitHub Actions schedule, and gives you all the benefits of local ECR images without waiting on a feature request. |
0 commit comments