言語を切り替える
テーマを切り替える

Dockerビルド高速化:キャッシュ活用でビルド時間を10倍速くする実践ガイド

タイポを1箇所直してdocker buildをやり直したら、またnpm installが始まった。10分経っても進捗バーは回ったまま。SNSを20件見て、コーヒーを2杯飲んでも、まだ終わらない。

コンテナ開発をしたことがある人なら、この絶望感はわかるはずです。

Dockerのキャッシュの仕組みを押さえれば、ビルド時間を10分から30秒に短縮できます。

30秒
ビルド時間
10分から30秒に短縮、20倍の高速化

この記事では、今すぐ使える3つのテクニック——.dockerignoreの設定レイヤーキャッシュの理解Dockerfile命令順序の最適化——と、上級編のBuildKitキャッシュマウントを紹介します。ビルドが遅いなら、ぜひ最後まで読んでください。

なぜあなたのDockerビルドはこんなに遅いのか?

ビルドコンテキストが大きすぎる

多くの人が躓く落とし穴から始めましょう:ビルドコンテキスト(Build Context)です。

docker build .を実行したとき、Dockerが最初に行うのはDockerfileの実行ではなく、.ディレクトリ下のすべてのファイルをDockerデーモンに送信することだということをご存知でしょうか?そう、すべてです。node_modulesも、.gitフォルダも、ダウンロードした数百MBのテストデータも含まれます。

私が見た中で最も極端な例は、フロントエンドプロジェクトのビルドコンテキストが800MBもありました。これらのファイルを転送するだけで2〜3分かかります。しかし実際にイメージに必要なのは、10MB未満のソースコードだけだったりします。

これは、本を1冊送りたいだけなのに、本棚ごと梱包して送るようなものです。

レイヤーキャッシュ無効化の連鎖反応

2つ目の問題:Dockerのレイヤーキャッシュメカニズムを理解していないこと。

Dockerイメージはレイヤー(層)で構成されています。Dockerfileの各命令(FROM、RUN、COPY)が1つのレイヤーを作成します。Dockerはビルド時に、各レイヤーで利用可能なキャッシュがあるかどうかをチェックします。命令が完全に同じで、依存ファイルも変更されていなければ、キャッシュをそのまま使用し、再実行しません。

素晴らしく聞こえますよね?

しかし問題は、あるレイヤーのキャッシュが無効になると、それ以降のすべてのレイヤーも再構築が必要になることです。ドミノ倒しのように、最初の1枚が倒れると、残りもすべて倒れます。

多くの人のDockerfileはこのように書かれています:

FROM node:18
COPY . /app
WORKDIR /app
RUN npm install

問題なさそうに見えますか?実は大問題です。

COPY . /appという行は、プロジェクト全体をコピーします。README.mdのタイプミスを修正するなど、ファイルを1つでも変更すると、このレイヤーのキャッシュは無効になります。その次は?後続のnpm installも再実行しなければなりません。

これが、コードを1行変更しただけで依存関係ツリー全体を再インストールしなければならなくなる理由です。

命令順序が非合理的

3つ目の落とし穴:命令の並べ方を知らないこと。

Dockerのキャッシュ戦略は単純です:上から順にチェックし、無効になった時点でキャッシュの使用を停止する。つまり、変更頻度の低い命令を前に、変更頻度の高い命令を後ろに配置する必要があります。

しかし現実には、多くのDockerfileが逆になっています。コードをコピー(最も頻繁に変更される)してから、依存関係をインストール(あまり変更されない)しています。その結果、コードを変更するたびに依存関係のキャッシュが全滅します。

要するに、何が頻繁に変わり、何が比較的安定しているかを把握していないのです。

テクニック1 - .dockerignoreでビルドコンテキストを削減

さて、問題点は明らかになりました。まずは最も簡単で即効性のある最適化、.dockerignoreについて話しましょう。

それは何?

.gitignoreを使ったことがありますよね?.dockerignoreも同じようなもので、Dockerにどのファイルをビルドコンテキストに含めないかを指示します。

作成方法は超簡単です。プロジェクトのルートディレクトリ(Dockerfileと同じ階層)に.dockerignoreというファイルを作成し、ルールを書き込むだけです。

Node.jsプロジェクトの設定例

私が実際に使用している設定を共有します:

# 依存関係ディレクトリ
**/node_modules/
**/npm-debug.log
**/.npm

# Git関連
.git/
.gitignore
.gitattributes

# テストとドキュメント
**/test/
**/tests/
**/docs/
**/*.md
!README.md

# IDEとエディタ
.vscode/
.idea/
*.swp
*.swo
.DS_Store

# 環境変数と設定
.env
.env.*
*.local

# ビルド成果物
dist/
build/
coverage/

いくつかのポイント:

  1. node_modulesは必ず除外する。これは数百MBになる可能性があり、イメージ内で再インストールされるため、ローカルからコピーする必要はありません。
  2. **/プレフィックスですべてのネストされたディレクトリにマッチさせる。例えば**/node_modules/./node_modules/./packages/lib/node_modules/の両方にマッチし、漏れを防ぎます。
  3. ディレクトリにはスラッシュを付けるnode_modules/はディレクトリを表し、node_modulesはファイルを表します。似ているようで、Dockerはこれを厳密に区別します。

効果は?

Next.jsプロジェクトで実測してみました:

  • 設定前:ビルドコンテキスト520MB、転送時間2分15秒
  • 設定後:ビルドコンテキスト4.8MB、転送システム3秒

そう、3秒です。一気に2分以上短縮されました。

これにはイメージサイズの削減分は含まれていません。.gitやnode_modulesを含めなくなったことで、最終的なイメージサイズも1.2GBから680MBにスリム化されました。

よくある落とし穴

罠1:.dockerignoreはビルドコンテキストのルートディレクトリでのみ有効です。docker build -f subfolder/Dockerfile .のように実行する場合、.dockerignoreはsubfolderではなくプロジェクトルートに置く必要があります。

罠2node_modules(スラッシュなし)と書くと効かないことがあります。node_modules/と書きましょう。

罠3:.gitの除外忘れ。.gitディレクトリは数百MBになることがありますが、イメージ内で必要になることはまずありません。

テクニック2 - Dockerレイヤーキャッシュの仕組みを理解して活用する

.dockerignoreで転送速度の問題は解決しましたが、核心はやはりキャッシュの仕組みを理解することです。

レイヤーキャッシュとは何か?

Dockerイメージはミルフィーユのようなもので、各レイヤーはDockerfileの1つの命令の実行結果です。

例えばこのDockerfile:

FROM node:18          # レイヤー1
RUN apt-get update    # レイヤー2
COPY package.json .   # レイヤー3
RUN npm install       # レイヤー4
COPY . .              # レイヤー5

ビルド時、Dockerは層ごとにチェックします:

  1. レイヤー1:FROM命令。ローカルにnode:18イメージがあるか?あればキャッシュを使用。
  2. レイヤー2:RUN命令。命令テキストが同じか?同じならキャッシュを使用。
  3. レイヤー3:COPY命令。package.jsonのチェックサムを計算。ファイルに変更がない?ならキャッシュを使用。
  4. レイヤー4:RUN命令。前の層がキャッシュ使用なら、引き続きチェック。
  5. レイヤー5:同様。

重要な点:あるレイヤーのキャッシュが無効になると、それ以降のすべてのレイヤーは再構築が必要になる

これが先ほど言ったドミノ倒し効果です。第3層でpackage.jsonを変更すると、第4層のnpm installと第5層のコードコピーも再実行されます。

キャッシュが効いているか確認する方法

ビルド出力を確認してください:

Step 3/5 : COPY package.json .
 ---> Using cache
 ---> 3a8f29e7c5b1

Using cacheと表示されていればキャッシュが使用されています。表示されていなければ?再構築されています。

また、docker history <イメージID>を使用してイメージのレイヤー履歴を確認でき、各レイヤーのサイズと作成時間が一目でわかります。

なぜCOPY命令は特殊なのか?

RUN命令はコマンドのテキストのみを見ます。RUN npm installというコマンドは、テキストが変わらない限り、Dockerはキャッシュを使用できると判断します。

しかしCOPYとADDは違います。Dockerはコピーされるファイルの内容のチェックサムを計算します。ファイル名が変わらなくても、内容が変わっていればキャッシュは無効になります。

これは賢い設計です。ファイルの内容が変われば、後続のビルドステップに影響する可能性があるため、古いキャッシュをそのまま使うわけにはいきません。

しかし、だからこそCOPY . .という書き方は危険なのです。プロジェクト内のファイルが1つでも(README.mdであっても)変われば、このレイヤーのキャッシュは無効になります。

テクニック3 - Dockerfile命令順序の最適化

キャッシュの原理がわかったところで、実践です。キャッシュ利用率を最大化するためにDockerfileをどう書けばいいでしょうか?

黄金律:安定的から変動的へ

核心はこの一言に尽きます:変更頻度の低い命令を前に、変更頻度の高い命令を後ろに置く

なぜか?Dockerは上から順にキャッシュをチェックするからです。前の層が安定していれば、後ろを変えても前のキャッシュには影響しません。

具体的には:

  1. ベースイメージ - ほぼ変わらない
  2. システム依存関係 - たまに変わる
  3. プロジェクト依存関係 - 時々変わる
  4. ソースコード - 毎日変わる

この順序で配置すれば、キャッシュ利用率を最大化できます。

悪い例:先にコードをコピーしてから依存関係をインストール

多くの人が最初はこう書いてしまいます:

FROM node:18
WORKDIR /app

# 間違い:先にプロジェクト全体をコピー
COPY . .

# その後で依存関係をインストール
RUN npm install

# 起動コマンド
CMD ["npm", "start"]

何が問題か?ソースコードを変更すると、COPY . .レイヤーのキャッシュが無効になり、後続のnpm installも再実行されます。

結果:JSコードを1行変えただけで、数百のnpmパッケージを再インストールすることになります。また10分コースです。

良い例:先に依存関係をインストールしてからコードをコピー

最適化した書き方:

FROM node:18
WORKDIR /app

# ステップ1:依存関係ファイルのみコピー
COPY package.json package-lock.json ./

# ステップ2:依存関係インストール(この層はキャッシュされる)
RUN npm ci --only=production

# ステップ3:ソースコードをコピー
COPY . .

# 起動コマンド
CMD ["npm", "start"]

このメリット:

  1. package.jsonが変わらない限り、npm ciの層はキャッシュを使用します。
  2. ソースコードを変更しても、COPY . .の層だけが無効になり、依存関係インストールのキャッシュは残ります。
  3. 2回目以降のビルドではnpmインストールがスキップされ、爆速になります。

実測では、この調整だけでビルド時間は7〜8分から30秒程度まで短縮されました。

他の言語でも同じ理屈

Pythonプロジェクト

FROM python:3.11
WORKDIR /app

# 先にrequirements.txtをコピー
COPY requirements.txt .

# pip installを実行
RUN pip install --no-cache-dir -r requirements.txt

# 最後にコードをコピー
COPY . .

Goプロジェクト

FROM golang:1.21
WORKDIR /app

# 先にgo.modとgo.sumをコピー
COPY go.mod go.sum ./

# 依存関係をダウンロード
RUN go mod download

# コードをコピー
COPY . .

# コンパイル
RUN go build -o main .

核心的な考え方は同じです:依存関係管理ファイルとソースコードを分けてコピーし、依存関係インストールのステップで可能な限りキャッシュを再利用することです。

上級テクニック:詳細なCOPY

プロジェクト構成が複雑な場合、さらに細かくすることもできます:

# 変更頻度の低い設定ファイルを先にコピー
COPY .eslintrc.json .prettierrc ./

# 依存ファイルをコピー
COPY package*.json ./
RUN npm install

# 共通ライブラリをコピー(もしあれば)
COPY ./lib ./lib

# 最後にビジネスロジックコードをコピー
COPY ./src ./src

これは少し珍しいやり方ですが、特定のシナリオ(monorepoプロジェクトなど)では非常に有効です。

上級編 - BuildKitキャッシュマウント

基礎的な最適化について話しましたが、次は少し進んでBuildKitのキャッシュマウントについて話しましょう。

BuildKitとは?

BuildKitはDocker 18.09で導入された新しいビルドエンジンで、旧エンジンより高速で、より強力なキャッシュ機能をサポートしています。

有効化は超簡単:

# 一時的に有効化
export DOCKER_BUILDKIT=1
docker build .

# またはコマンドの前に直接付ける
DOCKER_BUILDKIT=1 docker build .

Dockerのバージョンが新しい(19.03+)場合、BuildKitはデフォルトで有効になっているはずです。docker versionで確認できます。

キャッシュマウントとは?

前述のレイヤーキャッシュには問題があります:その層が無効になると、完全に再実行しなければなりません。

例えばpackage.jsonを変更して新しい依存関係を追加すると、npm installの層のキャッシュは消えます。その結果、以前にダウンロードしたパッケージも含めて、すべてのパッケージを再ダウンロードし直すことになります。

キャッシュマウントはこの問題を解決するためのものです。そのロジックは:レイヤーキャッシュが無効になっても、パッケージマネージャのダウンロードキャッシュは保持する

要するに、パッケージマネージャに永続的なキャッシュディレクトリを与え、ビルド間で共有させるのです。

使い方は?

Node.jsプロジェクトの例:

FROM node:18
WORKDIR /app

COPY package*.json ./

# ここが重要:npmキャッシュディレクトリをマウント
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

COPY . .
CMD ["npm", "start"]

--mount=type=cache,target=/root/.npmの部分がポイントです:

  • type=cacheはこれがキャッシュマウントであることを示します
  • target=/root/.npmはnpmのキャッシュディレクトリです

これがあれば、package.jsonが変わってレイヤーキャッシュが無効になっても、npmはすべてのパッケージをゼロからダウンロードする必要はありません。/root/.npmから既存のキャッシュを読み取り、新規または更新されたパッケージのみをダウンロードします。

他のパッケージマネージャの設定

Yarn:

RUN --mount=type=cache,target=/root/.yarn \
    yarn install --frozen-lockfile

pip (Python):

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

apt (システムパッケージ):

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    apt-get update && apt-get install -y gcc

aptのsharing=lockedパラメータに注意してください。aptはキャッシュへの排他的アクセスを必要とするため、並行ビルドの競合を避けるためにこのパラメータを追加します。

効果は?

200以上の依存関係を持つプロジェクトでテストした結果:

  • レイヤーキャッシュ無効だがキャッシュマウント有効:インストール時間が8分から1分30秒に短縮
  • 完全なコールドスタート(キャッシュ一切なし):やはり8分

つまり、キャッシュマウントはレイヤーキャッシュの「第二防衛ライン」です。レイヤーキャッシュが効いているときは最速(完全にスキップ)ですが、無効になったときでもキャッシュマウントが支えてくれるので、完全にゼロからダウンロードする必要がなくなります。

注意事項

  1. デフォルトの保持時間は長くない:BuildKitはデフォルトで2日以上経過し、かつ512MBを超えるキャッシュをクリーンアップします。CI/CD環境で使用する場合はポリシーの調整が必要かもしれません。
  2. すべてのシナリオで必要ではない:プロジェクトの依存関係が非常に少ない(十数個程度)場合、キャッシュマウントを追加してもあまり違いはありません。
  3. パスを間違えない:各パッケージマネージャのキャッシュディレクトリは異なるので、ドキュメントで確認してください。

結論

長々と話しましたが、核心は以下の3点です:

第1に、今すぐやること:プロジェクトルートに.dockerignoreファイルを作成し、node_modules、.git、テストファイルを除外してください。5分もかかりませんが、ビルドコンテキストを90%以上小さくできます。

第2に、今日中に変えること:Dockerfileの命令順序を調整してください。依存ファイルを先にCOPYし、RUNでインストールし、最後にソースコードをCOPYします。この調整で、2回目以降のビルド時間は10分から30秒に短縮されます。

第3に、時間があれば研究すること:プロジェクトの依存関係が多く、更新頻度が高い場合は、BuildKitのキャッシュマウントを試してみてください。レイヤーキャッシュが無効になったときの命綱になります。

私のプロジェクトでこの3ステップを実施したところ、ビルド時間は10分から30秒に、イメージサイズは1.2GBから680MBになりました。正直、この投資対効果は私が見た中で最高の最適化の一つです。

あなたのDockerビルドは遅いですか?遅いなら、この記事の手順を試してみてください。どれくらい速くなったか、コメントで教えてもらえるとうれしいです。

Dockerビルド高速化の完全な最適化フロー

ビルド時間を10分から30秒に短縮。レイヤーキャッシュ、.dockerignore、Dockerfile最適化を押さえる

Estimated time: PT30M

  1. 1

    Step 1: ビルドが遅い原因を理解する:ビルドコンテキストとレイヤーキャッシュ

    ビルドコンテキストの問題:
  2. 2

    Step 2: テクニック1:.dockerignoreでビルドコンテキストを減らす

    今すぐやること:プロジェクトルートに.dockerignoreを作成し、node_modules、.git、テストファイルを除外する。5分でビルドコンテキストを90%以上削減できる。
  3. 3

    Step 3: テクニック2:Dockerfileの命令順序を最適化

    今日中に変更:依存ファイルを先にCOPYし、RUNでインストール、最後にソースコードをCOPYする。2回目以降のビルド時間が10分から30秒になる。
  4. 4

    Step 4: テクニック3:BuildKitキャッシュマウントを使う

    時間があれば:依存が多く更新頻度が高い場合は、BuildKitのキャッシュマウントを試す。レイヤーキャッシュが無効になったときの切り札になる。

FAQ

なぜDockerビルドはこんなに遅いのですか?
ビルドコンテキストの問題:
• `docker build .`を実行すると、Dockerはまず`.`ディレクトリ配下のすべてのファイルをDockerデーモンに送ります
• node_modules、.gitフォルダ、テストデータなども含まれます
• フロントエンドプロジェクトではビルドコンテキストが800MBになることもあり、転送だけで2〜3分かかります
• 実際にイメージに必要なのは10MB未満のソースコードだけ、というケースも多いです
• 本を1冊送るのに本棚ごと梱包するようなものです

レイヤーキャッシュ無効化の連鎖反応:
• Dockerイメージはレイヤー構造で、Dockerfileの各命令(FROM、RUN、COPY)が1層を作ります
• ビルド時に各層でキャッシュが使えるかチェックし、命令と依存ファイルが同じならキャッシュを再利用します
• しかしある層のキャッシュが無効になると、それ以降のすべての層を再構築します。ドミノ倒しのように、最初の1枚が倒れると残りも倒れます

多くのDockerfileは次のように書かれています:FROM node:18、COPY . /app、WORKDIR /app、RUN npm install。この書き方だと、コードを少し変えるたびにnpm installが再実行されます。
.dockerignoreでビルドコンテキストをどう減らしますか?
今すぐやること:プロジェクトルートに.dockerignoreを作成し、node_modules、.git、テストファイルなどを除外します。5分もかからず、ビルドコンテキストを90%以上削減できます。

.dockerignoreの設定例:
• node_modules(依存パッケージを除外)
• .git(Git履歴を除外)
• *.log(ログファイルを除外)
• .env(環境変数を除外)
• dist(ビルド成果物を除外)
• test(テストファイルを除外)
• *.md(ドキュメントを除外)

設定後は、ビルドコンテキストが800MBから10MB未満に、転送時間が2〜3分から数秒に短縮されます。
Dockerfileの命令順序はどう最適化しますか?
今日中に変更:Dockerfileの命令順序を調整し、依存ファイルを先にCOPY、RUNでインストール、最後にソースコードをCOPYします。この変更で2回目以降のビルド時間が10分から30秒になります。

最適化の原則:
• 変更頻度の低い命令を前に(FROM、システム依存、アプリ依存)
• 変更頻度の高い命令を後ろに(COPYソースコード)

最適化前:
• FROM node:18
• COPY . /app
• WORKDIR /app
• RUN npm install
• コードを変えるたびにnpm installが再実行される

最適化後:
• FROM node:18
• WORKDIR /app
• COPY package*.json ./
• RUN npm install
• COPY . .
• package.jsonが変わったときだけnpm installが走り、コード変更は依存インストールに影響しない
BuildKitのキャッシュマウントはどう使いますか?
時間があれば:依存が多く更新頻度が高いプロジェクトでは、BuildKitのキャッシュマウントを試してください。レイヤーキャッシュが無効になったときの切り札になります。

BuildKitキャッシュマウント:
• --mount=type=cacheでキャッシュディレクトリをマウント
• npm installのnode_modulesなどをホストにキャッシュ
• 次回ビルドでキャッシュを直接使い、速度を10倍以上に

例:
• npm:RUN --mount=type=cache,target=/root/.npm npm install
• yarn:RUN --mount=type=cache,target=/root/.yarn yarn install --frozen-lockfile
• pip:RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
• apt:RUN --mount=type=cache,target=/var/cache/apt,sharing=locked apt-get update && apt-get install -y gcc

aptはキャッシュへの排他アクセスが必要なため、sharing=lockedを付けて並行ビルドの競合を避けます。
Dockerビルド最適化の効果はどのくらいですか?
最適化効果:
• ビルド時間が10分から30秒に(20倍高速化)
• イメージサイズが1.2GBから680MBに
• ビルドコンテキストが800MBから10MB未満に
• 転送時間が2〜3分から数秒に

私のプロジェクトでこの3ステップを実施した結果、ビルド時間は10分から30秒、イメージは1.2GBから680MBになりました。投資対効果は、私が見た中でも最高クラスの最適化のひとつです。

核心は3つ:
• 第1:今すぐ.dockerignoreを作成
• 第2:今日中にDockerfileの命令順序を変更
• 第3:時間があればBuildKitキャッシュマウントを検討

6分で読めます · 公開日: 2025年12月17日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます