目次
Docker ビルドキャッシュ入門:キャッシュの仕組みとイメージの軽量化
同じ Dockerfile でも、書き方によってビルド時間が 1 秒未満から数分に変わることがあり、イメージのサイズにも大きな差が出ます。この節では、実験を通じてキャッシュのルールとイメージを軽量化する方法を説明します。
キャッシュの仕組み
docker build は各コマンドを順番に処理します。各行について、Docker は次の点を確認します。このコマンドと、それより前のすべてのレイヤーは、前回のビルド時と同じですか?
- 同じ場合:キャッシュをそのまま使い、出力に
CACHEDと表示されます。 - 異なる場合:その行を再実行し、それ以降の行もすべてキャッシュが使われなくなります。
| コマンド | 判定基準 |
|---|---|
RUN | コマンド文字列が同じか(ネットワーク上のパッケージが更新されたかは確認しません) |
COPY / ADD | コピーするファイルの内容が同じか |
| その他 | コマンドと引数が同じか |
実験:手順の順序による影響
Express を使う小さなプロジェクト(package.json に express パッケージを記述)で、2 種類の Dockerfile を比較します。
悪い順序(Dockerfile.bad):
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
正しい順序(Dockerfile.good):
FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]
悪い順序:コードを1行変更する
まず一度ビルドし、次に server.js の1文字だけを変更してから、もう一度ビルドします。
$ docker build -f Dockerfile.bad -t cache-bad .
[+] Building 6.9s (9/9) FINISHED docker:default
=> [internal] load build definition from Dockerfile.bad 0.0s
=> => transferring dockerfile: 125B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.2s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/4] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 280B 0.0s
=> CACHED [2/4] WORKDIR /app 0.0s
=> [3/4] COPY . . 0.1s
=> [4/4] RUN npm install 5.4s
=> exporting to image 1.0s
=> => exporting layers 0.5s
=> => exporting manifest sha256:d27475f082a53018e303541bc1c0691ac25526e87761300e2aa8ec76d73e 0.0s
=> => exporting config sha256:19f2024df4329a5f2af23e50d8c18826f073d425e18133e9571c393999d913 0.0s
=> => exporting attestation manifest sha256:58b4730e9566f2ec7e8222cd41c14ea971d87272b6b0c190 0.0s
=> => exporting manifest list sha256:58b8fc79124f2fe21270801d5e40f09839c06ba5203304423125270 0.0s
=> => naming to docker.io/library/cache-bad:latest 0.0s
=> => unpacking to docker.io/library/cache-bad:latest 0.4s
[3/4] COPY . . は server.js の変更によって再実行され、その後の [4/4] RUN npm install も続けて再実行されました。すべてのパッケージを再ダウンロードしてインストールするのに、5.4 秒かかりました。
正しい順序:コードを1行変更する
初回のビルド(npm install の実行が必要です):
$ docker build -f Dockerfile.good -t cache-good .
[+] Building 5.7s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.good 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 95B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> [3/5] COPY package.json ./ 0.0s
=> [4/5] RUN npm install 4.4s
=> [5/5] COPY . . 0.1s
=> exporting to image 0.9s
=> => exporting layers 0.5s
=> => exporting manifest sha256:4b36d92a774b02cc1a188193e178a445b90f330b19f2dcc741d631e246e8 0.0s
=> => exporting config sha256:4a597aa667d9ba52eab36c4483f7507bbcae158b1b22af5ca6e4ac5b7020fa 0.0s
=> => exporting attestation manifest sha256:efea2ef65e2672718391b6419b9f5f2be50396163032a29e 0.0s
=> => exporting manifest list sha256:236918100fd2092d0388da5c03b32dfa3abaa2e62304abb3e3dd335 0.0s
=> => naming to docker.io/library/cache-good:latest 0.0s
=> => unpacking to docker.io/library/cache-good:latest 0.4s
server.js を変更してから、もう一度ビルドします:
$ docker build -f Dockerfile.good -t cache-good .
[+] Building 0.4s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.good 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 280B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package.json ./ 0.0s
=> CACHED [4/5] RUN npm install 0.0s
=> [5/5] COPY . . 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:6cd8c2671a295aab85724aefe72f8dc749f4d332224f5d534fff5e4e57fa 0.0s
=> => exporting config sha256:7df3d574ae91f98ea3634c4dcede63e64ce8018c902643c2db08c2b4dee4c3 0.0s
=> => exporting attestation manifest sha256:fc9ff7e57f4187c04b74446f5dd96e46ef9c7f0ea88372a6 0.0s
=> => exporting manifest list sha256:79e8a6d76ff2382f02bcadc183a44670959d1d0a30467a27b9d619e 0.0s
=> => naming to docker.io/library/cache-good:latest 0.0s
=> => unpacking to docker.io/library/cache-good:latest 0.0s
ビルド全体にかかった時間はわずか 0.4 秒です。COPY package.json と RUN npm install はどちらも CACHED で、最後の COPY . . だけが再実行されました。このプロジェクトのパッケージは1つだけですが、実際のプロジェクトでは数百のパッケージを扱うこともあり、npm install に数分かかる場合もあります。その場合、差はさらに大きくなります。
同じ考え方は、どの言語にも当てはまります:
| 言語 | 先にコピー | 次にインストール |
|---|---|---|
| Node.js | package.json、package-lock.json | npm ci |
| Python | requirements.txt または pyproject.toml | pip install |
| Go | go.mod、go.sum | go mod download |
| Java (Maven) | pom.xml | mvn dependency:go-offline |
| PHP | composer.json、composer.lock | composer install |
実験:同じレイヤー内で一時ファイルを削除する
各レイヤーは積み重なっています。後続の RUN でファイルを削除しても、前のレイヤーのサイズは減りません。curl をインストールする 2 通りの書き方を比べてみましょう。
Dockerfile.separate:
FROM debian:trixie-slim
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
RUN rm -rf /var/lib/apt/lists/*
Dockerfile.same:
FROM debian:trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
ビルド後にサイズを比較します。
$ docker images --filter reference=apt-separate --filter reference=apt-same --filter reference=debian:trixie-slim
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
apt-same:latest 5357e9775c1e 136MB 34.9MB
apt-separate:latest ad6c0a37d9ea 174MB 51.7MB
内容は同じ(どちらも curl をインストールし、パッケージリストを削除しています)ですが、apt-separate のほうが 38 MB 大きくなっています。原因を docker history で確認しましょう。
$ docker history apt-separate
IMAGE CREATED CREATED BY SIZE COMMENT
ad6c0a37d9ea 11 seconds ago RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #… 20.5kB buildkit.dockerfile.v0
<missing> 11 seconds ago RUN /bin/sh -c apt-get install -y --no-insta… 13.4MB buildkit.dockerfile.v0
<missing> 16 seconds ago RUN /bin/sh -c apt-get update # buildkit 21.6MB buildkit.dockerfile.v0
<missing> 8 days ago # debian.sh --arch 'amd64' out/ 'trixie' '@1… 87.7MB debuerreotype 0.17
$ docker history apt-same
IMAGE CREATED CREATED BY SIZE COMMENT
5357e9775c1e 1 second ago RUN /bin/sh -c apt-get update && apt-get… 13.4MB buildkit.dockerfile.v0
<missing> 8 days ago # debian.sh --arch 'amd64' out/ 'trixie' '@1… 87.7MB debuerreotype 0.17
apt-separate では apt-get update のレイヤーが 21.6 MB あります(ダウンロードしたパッケージリストです)。後続の rm -rf は新しいレイヤーでそれを「削除済みとしてマーク」するだけなので、元の 21.6 MB はイメージに残ります。同じ RUN にまとめれば、このレイヤーが終わる前にパッケージリストが削除されるため、どのレイヤーにも保存されません。
一時ファイルを残さないためによく使われる書き方は次のとおりです。
RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci && npm cache clean --force
RUN apk add --no-cache curl
.dockerignore
ビルドの開始時に、Docker はビルドコンテキスト全体(通常は現在のフォルダー)をビルドエンジンに送ります。node_modules を除外しない場合、Dockerfile 入門 の例では 30 MB を送信しますが、除外すれば 159 B だけです。.dockerignore の例:
.git
node_modules
dist
*.log
.env
.env.*
Dockerfile*
.dockerignore
ビルドが速くなるだけでなく、これらのファイルの変更によって COPY . . のキャッシュが無効になるのを防ぎ、.env や秘密鍵がイメージに含まれるのも防げます。
小さなベースイメージを選ぶ
イメージ のセクションで実測した Python 3.14 イメージのサイズ:
| ベースイメージ | ディスク使用量 |
|---|---|
python:3.14 | 1.63 GB |
python:3.14-slim | 180 MB |
python:3.14-alpine | 82.6 MB |
マルチステージビルド
コンパイラーや開発用パッケージが必要なのはビルド時だけです。最終イメージには含めないようにしましょう。マルチステージビルド は、サイズを縮小する最も効果的な方法です。
キャッシュマウント:パッケージのダウンロードを高速化する
BuildKit はキャッシュマウントをサポートしており、パッケージマネージャーのダウンロードキャッシュを複数回のビルドにわたって保持できます。RUN npm ci のレイヤーのキャッシュが無効になっても、すべてのパッケージをネットワークから再ダウンロードする必要はありません。
# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
キャッシュマウントの内容はイメージに含まれないため、イメージサイズは増えません。
キャッシュを使わずにビルドする
パッケージの最新バージョンを取得したい場合や、キャッシュに問題があると思われる場合は、次のようにします。
$ docker build --no-cache -f Dockerfile.good -t cache-good .
[+] Building 6.6s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.good 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 280B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> [3/5] COPY package.json ./ 0.0s
=> [4/5] RUN npm install 5.1s
=> [5/5] COPY . . 0.1s
=> exporting to image 1.0s
=> => exporting layers 0.4s
=> => exporting manifest sha256:30688d5c22069d97e9db0bd32d5939e8e99fda0c6a0c4af1f32fb5baacc9 0.0s
=> => exporting config sha256:e8e75108925e8f145e7367b8e36350a8175c3776154f560fb47a9fb287fe21 0.0s
=> => exporting attestation manifest sha256:adbbb2fc6683c9a35c67bb9237cf3afcfd24c39dd9aa4b55 0.0s
=> => exporting manifest list sha256:6b92b5704d4fa7a3f11cfda5ffde00e6516bdbf358e23dbece712a2 0.0s
=> => naming to docker.io/library/cache-good:latest 0.0s
=> => unpacking to docker.io/library/cache-good:latest 0.5s
--no-cache は各ステップを再実行します(CACHED は表示されません)。--pull も追加すると、ベースイメージに新しいバージョンがあるかどうかも再確認します。
ビルドキャッシュの確認と削除
ビルドキャッシュは次第に増えていきます。
$ docker builder du
ID RECLAIMABLE SIZE LAST ACCESSED
mr6xd3ylof28pr8obxiet3fo8 true 403B 3 minutes ago
l34l5occgcgh8bscivg2iusrn true 626B 3 minutes ago
... (中略)
Shared: 488.2MB
Private: 118.5MB
Reclaimable: 606.7MB
Total: 606.7MB
$ docker builder prune -f
ID RECLAIMABLE SIZE LAST ACCESSED
xqb9jtg0kmq0mdzb0vrmod68j true 20.67kB 3 minutes ago
u1pbndrekyh1q91qbb4n9e3fy* true 8.192kB 5 minutes ago
... (中略)
Total: 118.5MB
docker builder prune はビルドキャッシュだけを削除し、イメージやコンテナには影響しません。Docker のデータをすべて削除する方法は、ディスク容量のクリーンアップを参照してください。
まとめ
- あるレイヤーが変更されると、それ以降のすべてのレイヤーを再ビルドする必要があります。
- 変更頻度の低い手順(パッケージのインストール)を前に、変更頻度の高い手順(コードのコピー)を後に配置します。
- 一つの
RUN内でダウンロード、インストール、一時ファイルの削除を行うと、一時ファイルがイメージ内に残りません。 RUN --mount=type=cacheを使うと、パッケージのダウンロードデータを複数回のビルドで共有できます。
練習問題
練習 1:次の二つの Dockerfile の断片では、どちらのイメージが小さくなりますか? その理由も説明してください。
# A
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# B
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
解答
B のほうが小さくなります。A では rm が新しいレイヤーで実行されるため、パッケージリストは前のレイヤーに残ります。B では同じレイヤー内でダウンロードと削除を行うため、リストは保存されません。
練習 2:同じプロジェクトを docker build --no-cache とオプションなしでそれぞれ一度ビルドし、[+] Building に表示される合計時間を比較してください。
ヒント
オプションなしの場合、ほとんどの手順に CACHED と表示され、通常は 1 秒未満で完了します。--no-cache を指定すると、各手順が再実行されるため、所要時間はパッケージのインストール速度によって変わります。
よくある質問
RUN apt-get update がキャッシュされ、古いバージョンのパッケージがインストールされる場合はどうすればよいですか?
キャッシュはコマンド文字列だけを比較し、ネットワーク上のパッケージが更新されたかは確認しません。apt-get update と apt-get install を同じ RUN に記述し、必要に応じて docker build --no-cache で強制的に再ビルドします。
ビルドキャッシュがディスク容量を多く使っている場合は?
docker builder du で確認し、docker builder prune で削除します。実測については[ディスク容量を解放する](/ja/docker/cleanup/)を参照してください。
CI で毎回最初からビルドするのに時間がかかる場合は?
GitHub Actions の cache-from: type=gha などの外部キャッシュを使います。[GitHub Actions で自動ビルドする](/ja/docker/ci-cd/)を参照してください。
更新日 2026年10月14日