- Published on
Docker Hub の前にプロキシキャッシュを置く三つの方法 — registry:2、Nexus、ECR pull-through cache
- Authors

- Name
- Youngju Kim
- @fjvbn20031
問題は速度ではなく制限だ
CI が一回回るたびに FROM python:3.12-slim が Docker Hub から降りてくる。ノードが十台なら十回、パイプラインが一日に百回回れば百回。Docker Hub はそれを数えている。
Docker の文書が示す制限はこうだ。ログインしていないクライアントは IPv4 アドレス一つ(または IPv6 /64)あたり 6 時間に 100 回、個人アカウントでログインすれば 200 回、Pro・Team・Business は無制限。会社のオフィスやクラスタのように複数の機械が NAT の後ろから一つのアドレスで出て行くなら、その 100 回を全員で分け合う。
数字を文書だけで信じず、ヘッダを読んでみた。Docker が制限確認用に置いている ratelimitpreview/test リポジトリに匿名トークンでマニフェストを要求すると、応答ヘッダに残り回数が載る。
TOK=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -s -I -H "Authorization: Bearer $TOK" \
https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimit
ratelimit-limit: 100;w=3600
ratelimit-remaining: 85;w=3600
何度か実験する間に remaining は 91 → 88 → 85 と減った。ヘッダの窓(w=3600)は秒単位で 3,600 と書いてあるが、文書は 6 時間と言っている。どちらが正しいにせよ、この確認要求そのものも一回として数えられることは確かだ。
プロキシキャッシュはこの数字を守る装置だ。クラスタのすべてのノードが Docker Hub ではなく自分のキャッシュに聞けば、同じイメージは上流から 一度だけ 降りてくる。
最小の実験:registry:2 を pull-through キャッシュに
Docker が配布する registry:2 イメージは、環境変数一つで読み取り専用プロキシになる。
docker run -d --name regmirror -p 5001:5000 \
-v $PWD/data:/var/lib/registry \
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
registry:2
この状態で localhost:5001/library/nginx:1.27-alpine を引くと、キャッシュは無いレイヤを Docker Hub から受け取ってディスクに置き、クライアントに渡す。ローカルのデーモンに nginx のレイヤが一つも無いことを確認してから(docker images | grep nginx が 0 行)、同じイメージを三回引き、毎回ローカルの複製を消した。
| 試行 | 所要時間 | キャッシュディレクトリ | 備考 |
|---|---|---|---|
| 1 回目(キャッシュ経由) | 7.81 秒 | 22M(ブロブファイル 15 個) | キャッシュが空で上流から満たす |
| 2 回目(キャッシュ経由) | 3.95 秒 | 22M(変化なし) | ディスクから直接 |
| 3 回目(キャッシュ経由) | 4.01 秒 | 22M(変化なし) | ディスクから直接 |
| Docker Hub 直接 | 2.86 秒 | — | 比較用 |
docker images が報告するイメージの大きさは 76.8MB だが、キャッシュは 22M だ。キャッシュが保管するのは圧縮されたレイヤのブロブで、デーモンが報告するのは展開後の大きさだからだ。
実験が語ること
最初の pull は遅くなる。 上流から受け取り、ディスクに書き、また渡す二段階なので、直接引くこと(2.86 秒)より三倍近くかかった。
二回目以降も直接より速くはなかった。 家庭の回線で Docker Hub を直接引くのが 2.86 秒、キャッシュが満たされた後にプロキシを通すのが約 4 秒。プロキシコンテナが同じノートパソコンで動いていて、ホップが一つ多いことになる。回線が速くクライアントが一台だけなら、プロキシキャッシュは 速度のための装置ではない。
得られるのは上流への要求数だ。 2・3 回目の pull でキャッシュディレクトリは 1 バイトも増えなかった。レイヤは上流から受け取り直していないということだ。ノードが二十台のクラスタなら、Docker Hub が数える回数は二十ではなく一に近づく。これがプロキシキャッシュの価値だ。
正直に書いておくことが一つある。registry:2 は上流に出した要求を info ログに残さないので、pull ごとにマニフェスト確認を何回するかは数えられなかった。ディスクが増えなかったという事実から、レイヤの再ダウンロードが無かったことまでしか言えない。
Nexus:プロキシ、ホステッド、そしてグループ
registry:2 のプロキシは上流を一つしか見ない。会社ではふつう Docker Hub も見て、自分で作ったイメージも上げて、両方を一つのアドレスで使いたい。Sonatype Nexus Repository の Docker リポジトリは三種類ある。
- proxy — リモートリポジトリをキャッシュする。Docker Hub なら Remote storage に
https://registry-1.docker.ioを書き、Docker Index は「Use Docker Hub」を選ぶ(検索はindex.docker.ioが担当するので、二つを別々に書く)。 - hosted — 自分が push する場所。
- group — 複数の proxy・hosted を 一つの URL にまとめる。文書はグループを「ユーザーに読み取り権限ですべてのリポジトリを公開する推奨の方法」と呼び、メンバーは「望む順序で」入れる。グループに新しいリポジトリを入れれば、クライアント設定を変えなくてもすぐ見える。
クライアント側の設定は、Repository Connector のポート一つ(例:nexus.example.com:8082)をデーモンの mirror に登録すれば終わりだ。その後 docker pull nginx はグループ → メンバーの順に探し、hosted に無ければ proxy が Docker Hub から取ってきてキャッシュする。
Nexus が ECR を上流に 置くときは認証が特殊だ。ECR のトークンは 12 時間ものだが、Nexus の文書によれば最初の pull でそのトークンをキャッシュし、六時間ごとに更新する。AWS の資格情報を Nexus に一度渡せば、あとは Nexus が回す。
ECR pull-through cache:AWS の中でキャッシュする
クラスタが AWS にあるならキャッシュを別に運用する必要はなく、ECR がその仕事をする。ルール(pull through cache rule)に上流と接頭辞を書いておけば、<アカウント>.dkr.ecr.<リージョン>.amazonaws.com/<接頭辞>/library/nginx:1.27-alpine を引くときに ECR が上流から受け取り、自分のアカウントの ECR リポジトリに保管する。
文書で確認したルールはこうだ。
- 認証なしで使える上流は ECR Public、Kubernetes コンテナイメージレジストリ、Quay の三つ。Docker Hub、Azure Container Registry、GitHub・GitLab Container Registry、Chainguard は Secrets Manager のシークレットが必須で、別アカウントの ECR は IAM ロールで認証する。
- そのシークレットの名前は
ecr-pullthroughcache/で始まらなければならず、ルールと同じアカウント・リージョンにある必要がある。Docker Hub をアカウントで引くことになるので、匿名の 100 回ではなくそのアカウントの制限を使う。 - 同じタグをもう一度引くとき、過去 24 時間以内に上流と照合していれば上流に問い合わせず キャッシュをそのまま返す。窓が過ぎていれば新しい版を確認して更新する。タグを上書きして配備する習慣があるなら、一日まで古いイメージを受け取ることがある。
- 上流の更新に失敗しても、最後のキャッシュは引き続き降りてくる。Docker Hub が落ちても、すでに引いたイメージでの配備はできるということだ。
- キャッシュリポジトリにタグの不変性(immutability)を有効にすると、同じタグの更新が止まる。
- マルチアーキテクチャのイメージは、マニフェストリストとその中のアーキテクチャすべてを取ってくる。一つだけ欲しければ、そのアーキテクチャのダイジェストで引く。
- 最初の pull は ECR が上流に出て行く必要があるので、インターネットへの経路が要ることがある。PrivateLink エンドポイントしか無い VPC は最初の pull が失敗しうるので、文書は経路をあらかじめ用意するよう勧めている。以降の pull には要らない。
- AWS Lambda は pull-through cache ルールで作られたイメージをサポートしない。
どれを選ぶか
| registry:2 proxy | Nexus(proxy + group) | ECR pull-through cache | |
|---|---|---|---|
| 運用負担 | コンテナ一つ | サーバー一台とディスク管理 | なし(マネージド) |
| 上流 | 一つ | 複数をグループに | ルールごとに一つ |
| 自分のイメージの push | 不可(読み取り専用) | hosted へ | 通常の ECR リポジトリへ |
| 更新確認 | pull ごとに上流のマニフェストを確認 | 設定可能 | タグごとに 24 時間に一度 |
| 合う場所 | ホームラボ、ノード数台 | 社内、言語パッケージまで一か所に | AWS 内の EKS・ECS |
私たちのパイプラインでは
LabHub のビルドは Jenkins が kaniko でイメージを作って私設 Harbor に上げ、ArgoCD がそのタグを配備する。ベースイメージは Dockerfile にダイジェストまで固定してある。
FROM python:3.12-slim@sha256:78387bc3…
プロキシキャッシュを前に置いてもこの行はそのままだ。ダイジェストは内容のハッシュなので、どのキャッシュを経由しても同じバイトでなければ通らない。キャッシュが制限を守り、ダイジェストが内容を守る。二つの装置は別の仕事をしている。
一行で
プロキシキャッシュは pull を速くする装置ではなく、上流が数える回数を一つに減らす 装置だ。ノードが数台なら registry:2 で十分、リポジトリが複数なら Nexus のグループで一つのアドレスを作り、AWS の中なら ECR に代わりにキャッシュさせればよい。