Skip to content

Commit ddf7f22

Browse files
committed
Add post: Mirroring Docker Hardened Images to ECR with regclient
1 parent a66bb8c commit ddf7f22

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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

Comments
 (0)