2026年6月16日

2026年6月16日

Dockerのビルドキャッシュを活用する方法|命令の順番・キャッシュマウント・CIでの共有

Docker のビルドが毎回遅いときは、多くの場合「キャッシュが効く順番で Dockerfile が書かれていない」ことが原因です。変更の少ない処理を先に、変更の多いソースコードのコピーを後に置くだけで、2回目以降のビルドは大きく短くなります。この記事では、キャッシュが無効になる仕組み、Dockerfile の並べ方、BuildKit のキャッシュマウント、CI でキャッシュを共有する方法、キャッシュを捨てたいときの操作までを順に説明します。

キャッシュが効く仕組みと無効になる条件

Docker は Dockerfile の命令を上から1つずつ実行し、命令ごとの結果をレイヤーとして保存します。次のビルドでは、同じ命令に同じ入力が与えられていれば、保存済みの結果を再利用します。ビルド出力に CACHED と表示される行がそれです。

キャッシュが無効になる主な条件は次のとおりです。

  • 命令の文字列そのものが変わった(RUN のコマンドを1文字でも変えた)
  • COPY や ADD の対象ファイルの内容が変わった
  • その命令で参照している ARG の値が変わった
  • ベースイメージ(FROM)が更新された

そして最も重要なのが「ある命令でキャッシュが外れると、それより後の命令はすべて再実行される」というルールです。つまり、頻繁に変わる処理を上の方に置くと、その下にある重い処理まで毎回やり直しになります。

なお、RUN apt-get update のように外部の状態に依存する命令は、文字列が同じなら外部が更新されていてもキャッシュが使われます。キャッシュは「入力が同じなら結果も同じ」とみなす仕組みで、外部の変化は検知しません。

Dockerfile の並べ方を見直す

PHP(Composer)を使う WordPress 関連のアプリを例に、悪い順番と良い順番を比べます。

# 悪い例:ソースを1行でも変えると composer install からやり直しになる
# (git・unzip のインストールは省略)
FROM php:8.3-cli
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . .
RUN composer install --no-dev --no-interaction
# 良い例:依存定義ファイルだけを先にコピーしてインストール
FROM php:8.3-cli
RUN apt-get update \
 && apt-get install -y --no-install-recommends git unzip \
 && rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-scripts --no-autoloader
COPY . .
RUN composer dump-autoload --optimize

公式の php イメージには Composer がパッケージを展開するための unzip や git が入っていないため、良い例では先にインストールしています。良い例では、composer.json と composer.lock が変わらない限り、composer install の結果が再利用されます。普段の開発で変わるのはソースコードなので、再実行されるのは後半の COPY . . と軽いオートロード生成だけです。Node.js なら package.json と package-lock.json、Python なら requirements.txt で同じ考え方が使えます。

並べ方の原則をまとめると、次の順番になります。

  1. ベースイメージと OS パッケージのインストール(ほとんど変わらない)
  2. 言語のツールや依存定義ファイルのコピーとインストール(ときどき変わる)
  3. アプリのソースコードのコピー(頻繁に変わる)
  4. ビルド日時やコミット ID など毎回変わる値の埋め込み(最後に置く)

apt-get は1つの RUN にまとめる

RUN apt-get update \
 && apt-get install -y --no-install-recommends git unzip \
 && rm -rf /var/lib/apt/lists/*

apt-get update と apt-get install を別々の RUN に分けると、インストールするパッケージを追加したときに古い update の結果がキャッシュから使われ、パッケージが見つからないエラーの原因になります。同じ RUN にまとめておけば、パッケージ一覧を変えたときに update も一緒に再実行されます。

.dockerignore で不要な変更を持ち込まない

COPY . . は、ビルドコンテキスト内のファイルが1つでも変わるとキャッシュが外れます。.git ディレクトリ、ローカルの vendor や node_modules、ログファイルなどがコンテキストに含まれていると、ソースを変えていないのにキャッシュが外れることがあります。

# .dockerignore
.git
node_modules
vendor
*.log
.env

.env を除外しておくと、秘密情報がイメージに入ってしまう事故も防げます。

BuildKit のキャッシュマウントを使う

レイヤーのキャッシュは、依存定義ファイルが1行変わるとインストール全体をやり直します。そこで使えるのが BuildKit の「キャッシュマウント」です。パッケージマネージャーのダウンロードキャッシュをビルド間で保持できるため、依存が変わったときでも差分のダウンロードだけで済みます。Docker Engine 23.0 以降は Linux で BuildKit が標準のビルダーになっています(2026年10月現在)。

# syntax=docker/dockerfile:1
FROM php:8.3-cli
# (git・unzip のインストールは前の例と同じなので省略)
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
ENV COMPOSER_CACHE_DIR=/tmp/composer-cache
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer-cache \
    composer install --no-dev --no-interaction --no-scripts --no-autoloader
# Node.js の場合
RUN --mount=type=cache,target=/root/.npm npm ci

キャッシュマウントの中身は最終イメージには含まれないため、イメージサイズも増えません。注意点として、キャッシュマウントはビルダーごとのローカルな保存領域で、ディスクが不足すると削除されることがあります。「あれば速くなる」ものであって、無くてもビルドが成功する前提で使ってください。

Debian 系イメージの apt でキャッシュマウントを使う場合は、イメージに含まれる自動削除の設定(/etc/apt/apt.conf.d/docker-clean)を外し、並列ビルドに備えて sharing=locked を付ける書き方が Docker の公式ドキュメントで紹介されています。少し手順が増えるため、まずは言語のパッケージマネージャーから適用するのが手軽です。

マルチステージビルドでキャッシュの単位を分ける

ビルド用ツールと実行用イメージを分けるマルチステージビルドは、イメージを小さくするだけでなく、キャッシュの面でも効果があります。依存のインストールを独立したステージにしておくと、そのステージの入力が変わらない限り丸ごと再利用されます。また BuildKit は最終ステージに関係しないステージを実行しないため、テスト用ステージなどを同じ Dockerfile に置いても通常のビルドは遅くなりません。

CI でキャッシュを共有する

GitHub Actions などの CI では、ジョブごとにまっさらな環境で起動するため、ローカルのキャッシュがありません。そのままでは毎回フルビルドになるので、キャッシュを外部に保存して次回に読み込みます。

# レジストリにキャッシュを保存・読み込みする例(docker-container ドライバーが必要)
docker buildx create --use --name ci-builder --driver docker-container
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/my-app:buildcache \
  --cache-to type=registry,ref=registry.example.com/my-app:buildcache,mode=max \
  -t registry.example.com/my-app:1.0.0 --push .

mode=max は中間ステージのレイヤーもキャッシュとして保存する指定で、マルチステージビルドで効果が出やすくなります。GitHub Actions では type=gha を指定して Actions のキャッシュ領域に保存する方法もあります。使えるキャッシュの種類はビルダーのドライバーによって異なり、標準の docker ドライバーでは対応していないものがあります。エラーが出たら docker buildx ls でドライバーを確認してください。

キャッシュマウントの内容はこれらの外部キャッシュには含まれません。CI では「レイヤーの並べ方」と「外部キャッシュ」の効果が中心になります。

キャッシュを確認する・捨てる

キャッシュが効いているかは、ビルドログを詳しく表示すると確認できます。

# 各ステップの CACHED を確認
docker build --progress=plain -t my-app:dev .

# ビルドキャッシュの使用量を確認
docker buildx du
docker system df

# 古いキャッシュを削除(例:7日以上使われていないもの)
docker builder prune --filter until=168h

逆に、キャッシュのせいで古い状態のままになっていると疑われるときは、--no-cache で全体を作り直すか、--pull でベースイメージを最新に取り直します。特定のステージだけ作り直したい場合は --no-cache-filter <ステージ名> が使えます。セキュリティ更新を取り込むため、定期的に --pull --no-cache でビルドする運用にしておくと、キャッシュで古いパッケージが残り続けることを防げます。

docker builder prune はキャッシュを削除するだけでイメージやコンテナは消しませんが、docker system prune -a は使っていないイメージもまとめて削除します。本番サーバーで実行する前に、対象が何かを確認してください。

まとめ

  • キャッシュは命令ごとに効き、1か所外れるとそれ以降はすべて再実行されます。
  • 変わりにくい処理を上に、ソースのコピーを下に置き、依存定義ファイルだけを先にコピーしてインストールします。
  • apt-get update と install は同じ RUN にまとめ、.dockerignore で不要なファイルを除外します。
  • RUN --mount=type=cache でパッケージのダウンロードキャッシュをビルド間で保持できます。
  • CI では --cache-to / --cache-from で外部にキャッシュを保存し、定期的に --pull --no-cache でのビルドも行います。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲