「docker buildが遅い」「なぜかキャッシュが効かない」「イメージサイズが思ったより大きい」
原因のほとんどは、COPYの仕組みとビルドコンテキストの関係を理解していないことにある。
この記事では、DockerfileのCOPY命令がどのような順序で動き、ビルドコンテキストとレイヤーキャッシュにどう影響するかを実機出力をもとに解説する。「なんとなく書けるけど、なぜそう書くのかは知らない」という段階を抜け出したい方向けだ。
この記事のポイント
・COPY命令はビルドコンテキストからファイルを取得し、イメージに新しいレイヤーを積む
・docker buildの遅さの多くは「コンテキストが大きすぎる」ことが原因。.dockerignoreで削る
・COPYの順序を「変更頻度の低いものを先」にするとキャッシュが最大限に効く
・COPY --fromでマルチステージビルドのビルド成果物だけを本番イメージに取り込める
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
COPYとビルドコンテキストの関係を整理する
COPY命令の構文は単純だ。
COPY <src> <dest>
srcに書いたパスのファイルを、dest(コンテナ内のパス)へコピーする。ここで鍵になるのが「srcはどこから取得するのか」という点だ。
DockerfileのCOPYが参照できる範囲は、ビルドコンテキストと呼ばれる領域に限定される。ビルドコンテキストとは、docker buildコマンドで指定するディレクトリのことだ。
# カレントディレクトリ(.)をコンテキストとして指定 docker build . # 別のディレクトリをコンテキストとして指定 docker build /home/app/myproject
docker build .を実行した瞬間、DockerはカレントディレクトリのファイルをまるごとDockerデーモンに送信する。この「送信されたファイル群」がビルドコンテキストだ。COPYはこのコンテキストの中からしかファイルを取得できない。
重要なのは、コンテキストの送信はビルドが始まる前に行われるという点だ。デーモンが動いているのがリモートサーバーであれ、送信は必ず発生する。コンテキストが大きいほど、ビルド開始までの時間が長くなる。
ビルドコンテキストの仕組みを実機で確認する
1. コンテキスト送信のログを読む
Rocky Linux 9.4上でシンプルなPythonアプリをビルドした際の出力例を示す。
$ docker build -t myapp:v1 . [+] Building 12.3s (8/8) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 312B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.12-slim 2.1s => [internal] load build context 0.1s => => transferring context: 4.23kB 0.1s => [1/4] FROM docker.io/library/python:3.12-slim@sha256:... 0.0s => [2/4] WORKDIR /app 0.0s => [3/4] COPY requirements.txt . 0.0s => [4/4] RUN pip install --no-cache-dir -r requirements.txt 8.4s => exporting to image 0.9s
「transferring context: 4.23kB」の行がコンテキスト送信のサイズだ。適切に.dockerignoreを設定した結果、4KB程度に収まっている。同じプロジェクトで.dockerignoreを削除してみると、node_modulesや.gitディレクトリが含まれて300MB超のコンテキストになった。ビルドが遅い場合、まずここを確認する。
2. .dockerignoreでコンテキストを削る
.dockerignoreは.gitignoreと同じ書式で、ビルドコンテキストから除外するファイルやディレクトリを指定できる。
# .dockerignore の記述例 .git .gitignore node_modules __pycache__ *.pyc .env *.log tests/ docs/ README.md
特に注意が必要なのはnode_modulesと.gitディレクトリだ。どちらも数千~数十万ファイルを含むことがある。これらがコンテキストに含まれると、ファイルのinode走査だけで数秒かかることがある。
また、.envファイルをコンテキストから除外しておくことはセキュリティ上も重要だ。誤ってCOPY . .のような命令を書いた場合でも、.dockerignoreに.envがあればイメージに埋め込まれない。
COPYが積むレイヤーとキャッシュ設計
1. 1命令=1レイヤー
Dockerfileの各命令(FROM、RUN、COPY等)は、それぞれ独立したレイヤーをイメージに積む。COPY命令も例外ではなく、1行ごとに1レイヤーが作られる。
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . # レイヤー1 RUN pip install --no-cache-dir -r requirements.txt # レイヤー2 COPY . . # レイヤー3 CMD ["python", "app.py"]
この構造が重要なのはキャッシュの仕組みと直結しているからだ。Dockerは再ビルド時、前回と同一の命令かつ対象ファイルに変更がない場合、保存済みのレイヤーをそのまま使う(キャッシュヒット)。COPYの場合はコピー元のファイル内容を比較してキャッシュの有効性を判断する。
2. キャッシュが破棄されるパターン
あるレイヤーのキャッシュが無効になると、それ以降のすべてのレイヤーのキャッシュも破棄される。これを理解せずにDockerfileを書くと、毎回pip installやnpm installが走ることになる。
次のDockerfileが典型的なNG例だ。
# NG: アプリコードが変わるたびにpip installが走る FROM python:3.12-slim WORKDIR /app COPY . . # アプリ全体をコピー RUN pip install --no-cache-dir -r requirements.txt CMD ["python", "app.py"]
app.pyを1文字変更するだけで「COPY . .」のキャッシュが無効になり、続くpip installまで再実行される。
3. 変更頻度の低いものを先にCOPYする
解決策は依存関係ファイルを先にCOPYし、インストールを済ませてからアプリコードをCOPYする順序にすることだ。
# OK: requirements.txtが変わらない限り、pip installはキャッシュされる FROM python:3.12-slim WORKDIR /app COPY requirements.txt . # 変更頻度が低いファイルを先に RUN pip install --no-cache-dir -r requirements.txt COPY . . # アプリコードは後で CMD ["python", "app.py"]
この順序にすると、requirements.txtが変わらない限りpip installはキャッシュから復元される。開発中に何十回もdocker buildしても、パッケージのインストールは最初の1回だけになる。
実際にキャッシュが効いている場合のビルド出力はこうなる。
$ docker build -t myapp:v2 . [+] Building 1.2s (8/8) FINISHED => [internal] load build definition from Dockerfile 0.0s => [internal] load build context 0.0s => [3/4] COPY requirements.txt . CACHED => [4/4] RUN pip install --no-cache-dir -r... CACHED => [5/5] COPY . . 0.1s => exporting to image 0.3s
「CACHED」と表示されているレイヤーは再実行されていない。この設計ひとつで、開発中のビルド時間が大幅に短縮される。
COPY --fromでマルチステージビルドを活用する
1. ビルド成果物だけを本番イメージへ
COPY命令には--fromオプションがあり、同じDockerfile内の別ステージからファイルをコピーできる。これがマルチステージビルドの核心部分だ。
# ステージ1: ビルド環境 FROM golang:1.22 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app ./cmd/server # ステージ2: 実行環境(最終イメージ) FROM gcr.io/distroless/static-debian12 COPY --from=builder /app /app ENTRYPOINT ["/app"]
最終イメージにはGoのコンパイラもソースコードも含まれない。COPY --from=builder /app /appの1行で、builderステージでコンパイルしたバイナリだけを取り出している。これにより、本番イメージを数十MBオーダーに抑えられる。
2. 外部イメージからもCOPYできる
--fromにはステージ名だけでなく、Dockerイメージ名も直接指定できる。
# 公式イメージからcurlバイナリだけを取り込む例 FROM debian:12-slim COPY --from=curlimages/curl:8.7.1 /usr/bin/curl /usr/local/bin/curl
外部イメージのバイナリを流用するパターンは、Dockerfileを使ったコンテナ設計を体系的に学ぶ際に理解しておきたい応用技術の一つだ。
COPYとADDの使い分け
DockerfileにはADDという類似命令もある。両者の違いを理解しておこう。
| 命令 | 追加機能 | 推奨ケース |
|---|---|---|
COPY |
なし(純粋なコピーのみ) | ほぼすべての場合 |
ADD |
URLからの取得、tar自動展開 | tar展開が必要な特殊ケースのみ |
ADDはURLを指定してリモートからファイルを取得したり、.tar.gzを指定すると自動展開したりする機能を持つ。しかしこの「自動展開」が予期しない挙動を生むことがあるため、Dockerの公式ドキュメントも通常はCOPYを推奨している。
tar展開が必要な場合は、ADDではなくCOPY + RUN tar xfの組み合わせを使う方が意図が明確になる。
「no such file or directory」が出たときの原因調査
COPYで最も多いエラーが「COPY failed: file not found in build context or excluded by .dockerignore」だ。原因は大きく2つある。
原因1:ビルドコンテキスト外のパスを指定している
# NG: コンテキスト(.)の外にある親ディレクトリのファイルは参照できない COPY ../config/settings.yml . # エラー例 ERROR: failed to solve: failed to read dockerfile: failed to copy files: COPY failed: forbidden path outside the build context: ../config/settings.yml ("/config/settings.yml")
COPYはビルドコンテキストの外のファイルを絶対に参照できない。設計上の制約であり、回避する方法はない。コンテキストのルートを変えるか、ファイルをコンテキスト内に移動する必要がある。
原因2:.dockerignoreで除外されている
# .dockerignoreにこう書いてある場合 config/ # Dockerfileでこうしてもエラーになる COPY config/settings.yml . # ERROR: COPY failed: file not found in build context or excluded by .dockerignore
ファイルがコンテキストに存在しているのにCOPYが失敗する場合は、.dockerignoreを確認する。除外ルールに意図せずマッチしていることがある。特に*や**を使ったワイルドカードには注意が必要だ。
デバッグ手順
# コンテキストに含まれるファイルを確認する(.dockerignoreを考慮したリスト) docker build --no-cache --progress=plain . 2>&1 | grep "transferring context" # より詳細:最小限のDockerfileでコンテキストの中身をlsで確認 # Dockerfile.debug として保存 FROM busybox COPY . /ctx RUN ls -la /ctx docker build -f Dockerfile.debug . 2>&1 | grep -A 50 "RUN ls"
本記事のまとめ
| テーマ | 要点 |
|---|---|
| ビルドコンテキスト | docker build時に指定したディレクトリが丸ごとデーモンに送られる。.dockerignoreで不要ファイルを除外する |
| COPYの参照範囲 | コンテキスト内のファイルのみ。コンテキスト外(../)のパスは絶対に参照できない |
| レイヤーとキャッシュ | COPY命令は1行ごとにレイヤーを積む。変更頻度の低いファイルを先にCOPYするとキャッシュが最大化される |
| COPY --from | マルチステージビルドの別ステージや外部イメージからファイルを取得できる。本番イメージの軽量化に有効 |
| COPYとADD | 通常はCOPYを使う。ADDのtar自動展開は意図しない挙動を生むことがあるため、基本的に使わない |
| エラー原因 | 「no such file or directory」の大半は「コンテキスト外参照」か「.dockerignoreによる除外」。両方を確認する |
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
Dockerfileの設計原則からコンテナ運用のベストプラクティスまで、手を動かしながら学べる環境を用意している。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:Dockerfile postgresを組み込む設計パターン|初期化SQLと永続化の実装例
- この記事の属するカテゴリ:Dockerへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます