昨日、チームのメンバーからこんなメッセージが届きました:「先輩、docker pull nginxを15分も待ってるのにまだ終わらないんですが」。ベトナムをはじめ、国際回線が不安定な環境で働くすべての開発者が、一度は経験したことのある定番の問題です。
Docker Hubのサーバーはアメリカとヨーロッパにあります。docker pullのたびに、地球の裏側からデータを引き寄せることになります。Alpine(約5MB)なら問題ありません。しかしpytorch/pytorch(約6GB)は?CIで10〜15枚のイメージを並列取得するマイクロサービス一式は?そうなると国際回線の不安定さはもはや「不便」ではなく、開発を止める本物のブロッカーになります。
自分もEコマースプロジェクトのマイクロサービスデプロイ時にこの問題を経験しました。遅いだけでなく、あるときはコンテナのメモリリークに悩まされ、原因特定に2日もかかったことがあります。実はイメージが途中で切れ、レイヤーキャッシュが壊れ、いくつかのファイルが欠けた状態でコンテナが起動していたのに、明確なエラーが出なかったのです。それ以来、Registry MirrorとPull-through Cacheを本格的に調べ、根本から解決することにしました。
Registry MirrorとPull-through Cacheとは?
Registry Mirror
Registry mirrorは、DockerクライアントとDocker Hubの間に立つ中間サーバーです。docker pull nginx:latestを実行すると:
- DockerクライアントはDocker Hubではなくmirrorにリクエストを送信します
- mirrorにそのイメージがある場合(キャッシュヒット)→ 即座に返却、ローカルネットワーク並みの速度
- mirrorにない場合(キャッシュミス)→ mirrorがDocker Hubから取得してキャッシュし、クライアントに返します
Pull-through Cache
Pull-through cacheは名前ほど複雑ではありません:自分のregistryを経由してイメージを取得するたびに、自動的にキャッシュされます。次回同じイメージを取得するとき—同じネットワーク内の別マシンからでも—すべてローカルから取得でき、インターネットへのアクセスは不要です。
公開mirrorを使わず自前で構築する理由は?
- チーム全員が同じイメージを取得する際の帯域幅節約 — 10台が
node:20-alpineを取得しても、外部への帯域幅消費は1回分だけ - CI/CDパイプラインが大幅に高速化(自チームでは約8分から約90秒に短縮)
- Docker Hubのレート制限を回避 — フリーアカウントは100回/6時間が上限で、CIが頻繁に動くとすぐ上限に達する
- Docker Hubで障害が起きても独立して動作可能
実践:Registry Mirrorを設定する3つの方法
方法1:既存のmirrorを使う(最速)
とにかく素早く解決したいなら、公開mirrorを使う方法があります。デメリット:長期的に不安定になりがちで、稼働時間を自分でコントロールできません。/etc/docker/daemon.jsonを開くか新規作成します:
sudo nano /etc/docker/daemon.json
以下の内容を追加します:
{
"registry-mirrors": [
"https://mirror.gcr.io"
]
}
Dockerを再起動して確認します:
sudo systemctl daemon-reload
sudo systemctl restart docker
# 設定が適用されたことを確認
docker info | grep -A 5 "Registry Mirrors"
方法2:Docker Registryでオリジナルのpull-throughキャッシュを構築する
自チームで現在使っているソリューションです—安定していて完全に制御でき、社内サーバーやVPSがすでにあれば無料です。Dockerが動くマシンが1台あれば十分です。
ステップ1:registryの設定ファイルを作成する
mkdir -p ~/docker-mirror && cd ~/docker-mirror
nano config.yml
config.ymlの内容:
version: 0.1
log:
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
proxy:
remoteurl: https://registry-1.docker.io
# Docker Hub認証が必要な場合はコメントを解除してください(ProアカウントまたはPrivateイメージ用)
# username: your_dockerhub_username
# password: your_dockerhub_password
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
ステップ2:Docker ComposeでRegistryを起動する
docker-compose.ymlを作成します:
version: "3.8"
services:
registry-mirror:
image: registry:2
container_name: docker-mirror
restart: always
ports:
- "5000:5000"
volumes:
- ./config.yml:/etc/docker/registry/config.yml:ro
- registry-data:/var/lib/registry
volumes:
registry-data:
起動して確認します:
docker compose up -d
# registryが稼働していることを確認
curl http://localhost:5000/v2/
# 期待される結果:{}
# ログを確認
docker logs docker-mirror -f
ステップ3:Docker daemonがmirrorを参照するよう設定する
クライアントマシン(同じサーバーでも同じネットワーク内の別マシンでも可)で/etc/docker/daemon.jsonを編集します:
{
"registry-mirrors": [
"http://YOUR_SERVER_IP:5000"
],
"insecure-registries": [
"YOUR_SERVER_IP:5000"
]
}
YOUR_SERVER_IPをregistryを実行しているサーバーの実際のIPアドレスに置き換えてから、Dockerを再起動します。
sudo systemctl daemon-reload
sudo systemctl restart docker
ステップ4:動作確認して違いを体験する
# 初回のイメージ取得(キャッシュミス — mirrorを経由してDocker Hubから取得)
time docker pull nginx:alpine
# ローカルのイメージを削除
docker rmi nginx:alpine
# 2回目の取得(キャッシュヒット — ローカルmirrorから取得)
time docker pull nginx:alpine
2回目は明らかに速くなります。自分のローカルネットワークでは、Alpineイメージの取得時間が約45秒から約3秒に短縮されました。
方法3:Nginx reverse proxyでHTTPS化(本番環境対応)
HTTPを本番環境で使うと特定の問題が発生します:各クライアントマシンのdaemon.jsonにinsecure-registriesを追加しなければなりません。開発者マシンが20台あると、これはすぐに悪夢になります。Let’s EncryptでNginx reverse proxyを設定すれば、まとめて解決できます:
# certbotをインストール(Ubuntu/Debian)
sudo apt install certbot python3-certbot-nginx -y
# 証明書を取得
sudo certbot --nginx -d registry-mirror.yourdomain.com
/etc/nginx/sites-available/registry-mirrorにNginxを設定します:
server {
listen 443 ssl;
server_name registry-mirror.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/registry-mirror.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry-mirror.yourdomain.com/privkey.pem;
client_max_body_size 0; # サイズ制限なし — 大きなイメージに必要
chunked_transfer_encoding on;
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900; # 15分 — 大きなイメージに必要
}
}
HTTPSが使えるようになれば、各クライアントマシンのdaemon.jsonはとてもシンプルになります:
{
"registry-mirrors": [
"https://registry-mirror.yourdomain.com"
]
}
insecure-registriesはもう不要です。
キャッシュの容量管理
キャッシュは思った以上に早く蓄積されます。自チームのregistryは3週間で約25GBを占めていました—主にNodeとPythonのベースイメージと、同一イメージの複数タグが原因でした。知っておくべきコマンドをいくつか紹介します:
# 現在のキャッシュ使用量を確認
docker exec docker-mirror du -sh /var/lib/registry
# ガベージコレクション — 参照されていないレイヤーを削除
docker exec docker-mirror registry garbage-collect \
/etc/docker/registry/config.yml
# キャッシュを完全にリセット(すべて削除)
docker compose down
docker volume rm docker-mirror_registry-data
docker compose up -d
まとめ
多くのチームがregistry mirrorを検討し始めるのは、CIパイプラインのビルド完了を20分待った後か、月末の帯域幅の請求書を見てからです。小さなサーバー、月額5ドルのVPSでさえあれば、その両方の問題をチーム全体のために解決できます。
自分のセットアップでの具体的な数字:CI/CDのスピンアップ時間が約8分から約90秒に短縮。2週間スプリントで20〜30回デプロイすれば、毎月何時間もの節約になります—ターミナル画面が止まったままひたすら待つストレスは言うまでもありません。
実装前の重要な注意点:このmirrorはDocker Hubのパブリックイメージのみキャッシュします。チームがGitHub Container Registry(ghcr.io)、AWS ECR、Google Container Registryも使用している場合は、それぞれに対して別のmirrorを構築する必要があります—proxy.remoteurlを対応するアドレスに変更し、異なるポートで起動してください。

