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

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

深夜1時、私はターミナルのプログレスバーを見つめていました。タイプミスを修正してdocker buildを再実行、そしてまた——npm installが始まりました。10分が経過し、SNSを20回チェックし、コーヒーを2杯飲みましたが、プログレスバーはまだ動いています。

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

しかし、実はこれには解決策があります。私はDockerのキャッシュメカニズムを1週間研究し、ビルド時間を10分から30秒に短縮しました。正直なところ、初めてその効果を見たときは数秒間呆然としました——Dockerってこんなに速かったのか、と。

30秒
ビルド時間

この記事では、すぐに使える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: 30 min

  1. 1

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

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

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

    よくある間違い:
    • FROM node:18
    • COPY . /app
    • WORKDIR /app
    • RUN npm install
    • これだとコードを変更するたびにnpm installが再実行される
  2. 2

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

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

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

    設定後:
    • ビルドコンテキストが800MBから10MB未満に
    • 転送時間が2〜3分から数秒に
  3. 3

    Step3: テクニック2: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が走る。コード変更は依存インストールに影響しない
  4. 4

    Step4: テクニック3: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を付ける)

FAQ

なぜDockerビルドがこんなに遅いのですか?
ビルドコンテキストの問題:
• docker build .を実行すると、Dockerはまず.ディレクトリ下の全ファイルをDockerデーモンに送信します。
• node_modules、.gitフォルダなどが含まれると数百MBになり、転送だけで時間がかかります。
• 実際に必要なのはソースコードだけの場合が多いです。

レイヤーキャッシュ無効化の連鎖反応:
• Dockerは層ごとにキャッシュをチェックしますが、ある層のキャッシュが無効になると、それ以降の全層が再構築されます。
• 多くのDockerfileは、依存関係インストールの前にソースコード全体をコピーしているため、コードを1行変えるだけで依存関係の再インストールが発生します。
.dockerignoreはどう設定すればいいですか?
今すぐ実行:プロジェクトルートに.dockerignoreファイルを作成し、不要なファイルを除外します。

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

設定すると、ビルドコンテキストが劇的に小さくなり、転送時間が数秒になります。
Dockerfileの命令順序はどう最適化すべきですか?
今日中に変更:依存ファイルを先にCOPYし、RUNでインストール、最後にソースコードをCOPYするように順番を変えてください。

最適化の原則:
• 変更頻度の低い命令(FROM、依存インストール)を前に
• 変更頻度の高い命令(COPYソースコード)を後ろに

最適化後:
• COPY package*.json ./
• RUN npm install
• COPY . .
これで、package.jsonが変わった時だけnpm installが走り、コード変更は依存インストールに影響しなくなります。
BuildKitキャッシュマウントとは何ですか?
時間があれば:依存関係が多く更新頻度が高い場合におすすめの機能です。

BuildKitキャッシュマウント:
• --mount=type=cacheを使ってキャッシュディレクトリをマウントします。
• パッケージマネージャ(npm, pip等)のキャッシュをホストに保存します。
• レイヤーキャッシュが無効になっても、ダウンロードキャッシュを利用できるため、ゼロからダウンロードする必要がなくなります。

例:RUN --mount=type=cache,target=/root/.npm npm install
最適化の効果はどれくらいですか?
最適化効果:
• ビルド時間が10分から30秒に(20倍高速化)
• イメージサイズが1.2GBから680MBに削減
• ビルドコンテキストが800MBから10MB未満に
• 転送時間が2〜3分から数秒に

私自身のプロジェクトでこの3ステップ(.dockerignore、命令順序、キャッシュマウント)を実施した結果、劇的な改善が見られました。投資対効果が非常に高い最適化です。

6 min read · 公開日: 2025年12月17日 · 更新日: 2026年1月22日

コメント

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

関連記事