Blogへ戻る

Language: 日本語

「コンテナのデプロイ待ち、長すぎませんか?」 Docker Buildx の二重転送解消とレイヤーキャッシュで CD を高速化する実践

開発環境(dev)への継続的デプロイ(CD)パイプラインを運用していると、**「コードを少し直しただけなのに、コンテナのビルドとプッシュ待ちで毎回数分間待たされる……」** という開発者体験の悪化に直面します。

開発環境(dev)への継続的デプロイ(CD)パイプラインを運用していると、「コードを少し直しただけなのに、コンテナのビルドとプッシュ待ちで毎回数分間待たされる……」 という開発者体験の悪化に直面します。

Cloud Run へのリビジョン反映やロールバックの落とし穴(『「デプロイは Green なのにコードが変わっていない?」 Cloud Run CD で最新リビジョンが反映されない罠』)と並行して、コンテナイメージのビルドとレジストリ配信パイプラインの徹底的なボトルネック調査を行いました。

調査の結果、「イメージの二重転送」「不要な自己コピー」「レイヤーキャッシュの未活用」 という 3 つの無駄が重なり、パイプラインの時間を大幅に浪費していることが判明しました。

この記事では、Docker Buildx の転送フロー改善やイメージの軽量化、GitHub Actions キャッシュの最適化によって、CD パイプラインを劇的に高速化した実践手順を解説します。

得られること

  • docker buildx --loaddocker push の二重転送ボトルネックとその解消法がわかる
  • コンテナイメージのマルチステージ最適化でサイズを半分以下に削ぎ落とす手法が学べる
  • GitHub Actions の Buildx レイヤーキャッシュと不要な自己コピー排除によるビルド高速化の勘所が身につく!

1. --loaddocker push による「二重転送」の罠

最も大きな時間的ロスを生み出していたのは、CI ワークフローにおける 「コンテナイメージの二重転送」 でした。

初期の実装では、Buildx でビルドした成果物を一度ローカルの Docker daemon に --load してから、レジストリ(Artifact Registry / ECR 等)へ docker push していました。

【 改善前の二重転送フロー 】
1. docker buildx build --load  (約 67 秒: Buildx ビルダーからローカル Docker daemon へ展開)

2. docker push <registry/image> (約 40 秒: ローカル Docker daemon からリモートレジストリへ送信)

合計で約 107 秒の転送待ちが発生!

ウォームキャッシュ(キャッシュが効いている状態)であっても、「ビルダー ➔ ローカル daemon」への展開と「ローカル daemon ➔ リモートレジストリ」への送信という二重の I/O だけで 107 秒 を消費していました。

CI ランナーの目的は「レジストリにイメージを格納すること」であり、CI ランナー自身のローカル Docker daemon でイメージを実行する必要は一切ありません。

--push による直接配信への切り替え

そこで、Buildx の --push フラグを使い、ビルダーから直接リモートレジストリへストリーミング送信 する構成に変更しました。

【 改善後の直接 push フロー 】
docker buildx build --push  (ビルダーからリモートレジストリへ直結 ⚡️)

この変更だけで、バックエンドの dev CD 全体の所要時間が 3 分 48 秒から 2 分 02 秒(約 46% 短縮) へと半減しました。無駄な中間ホップをなくすだけで、1 回のデプロイあたり約 1 分 46 秒の短縮になります。

2. マルチステージビルドによるイメージの大幅スリム化

転送するイメージ自体のサイズが小さければ、ビルドもネットワーク転送も Cloud Run 側のコンテナ起動(コールドスタート)もすべて高速化されます。

バックエンドの Dockerfile を見直し、開発用ツール(TypeScript コンパイラ、テストランナー、中間ビルド成果物)を本番イメージから徹底的に排除しました。

# ビルドステージ
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 本番実行ステージ(軽量化)
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist

USER node
CMD ["node", "dist/index.js"]

この最適化により、バックエンドのイメージサイズは 365 MB から 153 MB(約 58% 削減) までスリム化されました。

3. CI 側のビルド待ちを削る 2 つの最適化

さらに、PR ごとの CI や CD パイプラインにおけるビルド待ち時間を削るため、2 つの改善を加えました。

① GitHub Actions キャッシュバックエンドの導入

GitHub Actions のキャッシュストレージを活用する type=gha レイヤーキャッシュを Buildx に導入しました。

- name: Build and push Docker image
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

これにより、依存パッケージに変更がない場合は中間レイヤーの再ビルドがスキップされ、キャッシュヒット時のビルドが数秒で完了するようになります。

② フロントエンドの「builder 自己コピー」削除

フロントエンドの Dockerfile を精査したところ、マルチステージビルドの最終ステージで COPY --from=builder /app /app のように不要なディレクトリ全体を丸ごとコピーしている箇所を発見しました。

必要な静的成果物(dist.next/standalone)のみをピンポイントでコピーするよう修正したことで、毎ビルドで発生していた約 23 秒の無駄なファイル I/O がゼロ になりました。

まとめ: 転送回数を減らし、転送するものを小さくする

コンテナのビルド・デプロイパイプラインを高速化する原則は、非常にシンプルです。

  1. 転送回数を減らす: --loadpush の二重転送をやめ、buildx --push で直接レジストリへ届ける
  2. 転送するものを小さくする: マルチステージビルドで本番成果物だけを残し、イメージサイズを削ぎ落とす
  3. 作り直すものを減らす: type=gha キャッシュの導入と不要なファイルコピーの排除でビルドステップを最小化する

デプロイパイプラインの数分の短縮は、日々の開発サイクル全体のスピードと開発者の集中力維持に直結します。

参考リンク