Docker 基礎 #6 .dockerignore とビルドコンテキスト — キャッシュをうまく使う

読了 8分

Docker 基礎シリーズの最後の記事です。ここまで小さなアプリをコンテナ化して、回して、データを生かして、レジストリに上げてきました。この記事は一箇所だけ深く — イメージがなぜ膨れるのか、ビルドがなぜ遅くなるのか、そしてその二つの問題を解く二つの道具 .dockerignoreレイヤーキャッシュ です。

Docker 基礎 シリーズでこの記事の位置:

ビルドコンテキスト — Docker がビルド前にしていること #

docker build -t myapp . を叩くとき、最後の点 (.) が ビルドコンテキスト です。Docker はビルドを始める前にこのディレクトリ全体を丸ごとデーモンに送ります。

ビルドコンテキストの流れ
host                              docker daemon
┌──────────────────┐              ┌─────────────────┐
│  ./hello-docker  │              │                 │
│  ├ app.py        │   tar で束ね │                 │
│  ├ requirements  │ ───────────▶ │   build         │
│  ├ Dockerfile    │              │                 │
│  ├ ...           │              │                 │
│  └ .git/         │              └─────────────────┘
└──────────────────┘

ビルド出力でよく見るこの行がその段階です。

ビルド出力の一行
=> [internal] load build context           500ms
=> => transferring context: 152MB

この 152MB が大きいほど:

  • ビルド開始までに時間がかかる
  • Docker デーモンがメモリ / ディスクに一時的に保持する必要がある
  • 変更検出 (キャッシュキー計算) がより重くなる
  • 何より — この中に入ったファイルが COPY . . のような命令で イメージにそのまま刻まれる可能性がある

ここで二つを分離して覚えておく必要があります。

  1. コンテキストとして送られるもの — Docker デーモンが受け取る全ファイル
  2. イメージに入るものCOPY / ADD で明示的に持ってきたものだけ

.dockerignore はこのうち 第一段階 を削ります。

.gitnode_modules.venv — 何を除外するか #

小さなディレクトリでも一度見ると驚きます。

コンテキストサイズを見る
du -sh .
# 152M    .

du -sh .git node_modules .venv build dist *.log
# 80M     .git
# 60M     node_modules     # コンテナの中で入れ直すから不要
# 8M      .venv            # 同じく
# 4M      build
# 1M      dist
# 200K    *.log

このうちコンテナに必要なものはほとんどありません。依存は RUN pip install / RUN npm ci がコンテナの中で入れ直すので、ホストの node_modules.venv を送る理由はゼロ です。.git も普通は除外します (ビルドで git 情報が必要なら --build-arg GIT_SHA=... で明示的に注入する方が綺麗)。

.dockerignore を書く #

.gitignore とほぼ同じ文法です。Dockerfile の隣に .dockerignore というファイルを作って、ビルドコンテキストから除外したいパターンを書きます。

.dockerignore (Python/Node 共通ベース)
# バージョン管理
.git
.gitignore

# 環境 / 秘密
.env
.env.*
*.key
*.pem

# ビルド成果物
dist/
build/
out/
*.egg-info/

# 依存 (コンテナの中で入れ直される)
node_modules/
.venv/
__pycache__/
*.pyc

# エディタ / OS
.vscode/
.idea/
.DS_Store
Thumbs.db

# ログ
*.log
logs/

# テスト / キャッシュ
.pytest_cache/
.mypy_cache/
.ruff_cache/
coverage/
.coverage
htmlcov/

# コンテナ自体のファイル
Dockerfile.dev
docker-compose*.yml
.dockerignore

Dockerfile 自体と .dockerignore もコンテキストに入ります — わざわざイメージの中に刻む理由がないので無視パターンに入れておく方が綺麗です。

マッチング文法を一度に #

パターン意味
node_modulesどの位置でもこの名前のファイル / ディレクトリ
node_modules/ディレクトリだけ
*.log全ての .log
**/*.logあらゆる深さの .log (Docker はデフォルトで再帰なので *.log と事実上同じ)
dist/**dist の中の全て
!important.log上のルールの中でこのファイルは例外で含む

! 例外ルールは強力ですが間違いやすいです。シンプルな除外中心 で書くのが安全です。

効果確認 #

追加前後でコンテキストサイズがどれだけ減ったかビルド出力にすぐ出ます。

追加前
=> [internal] load build context           1.2s
=> => transferring context: 152MB

# .dockerignore 追加後
=> [internal] load build context           120ms
=> => transferring context: 240kB

体感できる差は大きいです。CI が毎回コンテキストをビルドマシンに引っ張ってくると考えると明白な節約です。

レイヤーキャッシュ — ビルドを速く #

Docker ビルドの二つ目の核がキャッシュです。Dockerfile の一行 (== レイヤー一枚) ごとに Docker はこんな判断をします。

この命令の入力 (前のレイヤー + 命令自体 + 命令が参照するファイル) が前回のビルドと同じか? → 同じならキャッシュ再利用、違えばこのレイヤーから再ビルド。

このモデルがビルド速度に決定的です。そして — 一つのレイヤーが壊れるとそれ以下の全てのレイヤーが一緒に壊れます。 (上から下に積み上がるので。)

キャッシュが壊れる箇所 #

間違った順序 — キャッシュがよく壊れる
FROM python:3.14-slim
WORKDIR /app

COPY . .                              # コードと依存定義が一緒に入る
RUN pip install -r requirements.txt   # コードを一文字変えるだけで上が壊れて入れ直し

この Dockerfile は コード一行を変えるだけで依存を最初から入れ直します。 COPY . . の結果が変わるので次の RUN のキャッシュキーも変わるからです。依存が大きいプロジェクトならビルドが毎回分単位で長くなります。

正しい順序 — 依存とコードを分離 #

推奨順序
FROM python:3.14-slim
WORKDIR /app

# 1) 依存定義だけ先にコピー
COPY requirements.txt .
# 2) 依存インストール — requirements.txt が変わらなければキャッシュ再利用
RUN pip install --no-cache-dir -r requirements.txt

# 3) その後にコードコピー
COPY . .

CMD ["python", "app.py"]

これで app.py だけ変えれば:

  • COPY requirements.txt . → キャッシュ再利用
  • RUN pip install ...キャッシュ再利用 (依存インストールをスキップ)
  • COPY . . → 再実行
  • ビルドが数秒で終わる

変わらないものを上に、よく変わるものを下に — これが Dockerfile を書くときの最初のヒューリスティックです。ベースイメージ (ほぼ変わらない) → システム依存 → 言語依存 → コード (よく変わる) の順序。

Node.js も同じパターン #

Node 依存キャッシュ
FROM node:20-slim
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
CMD ["node", "server.js"]

package.json / package-lock.json だけ先にコピー → npm ci で入れて → その後コード。同じパターンです。

ビルドキャッシュの副産物 — イメージサイズ #

レイヤーキャッシュはビルド速度だけでなく イメージサイズ にも影響します。一つのレイヤーがディスクに刻まれると、その中で作った一時ファイルも一緒に固まります。

よくある間違い — 一時ファイルがレイヤーに刻まれる
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

この三行は 三つのレイヤー になります。apt-get update が作ったインデックスが最初のレイヤーに刻まれて、その後 apt-get clean は別のレイヤーで消してもイメージの中にはそのまま残ります。

同じことを一行にまとめると一レイヤーになります。

推奨パターン
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

&& でまとめて、最後にキャッシュディレクトリを消して一レイヤーの中で掃除まで終えます。イメージサイズの差が明確です。

分けた場合まとめた場合
イメージサイズ~180MB~85MB

このパターンはほぼ全てのベースイメージの公式 Dockerfile で見られます。

--no-cache — 強制的に新しくビルド #

キャッシュが意図せず古い結果を掴んでいるとき:

キャッシュ無視ビルド
docker build --no-cache -t myapp .

またはベースイメージ自体を新しく取りたいとき:

ベースも新しく取る
docker build --pull -t myapp .
docker build --pull --no-cache -t myapp .   # 両方

apt-get update で取ったインデックスが古くてセキュリティパッチを受けられないことが起こります。CI の定期ビルドではたまに --no-cache または --pull を強制するオプションを置きます。

シリーズの締め — 一コンテナの一サイクル #

基礎シリーズ 6 編を通して作った流れを一箇所にまとめます。

一箇所にまとめた Dockerfile
FROM python:3.14-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

# システム依存 (一レイヤー、掃除まで)
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 言語依存 (あまり変わらない — 上に)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# コード (よく変わる — 下に)
COPY app.py .

EXPOSE 8000
CMD ["python", "app.py"]
.dockerignore
.git
.env
.env.*
node_modules/
.venv/
__pycache__/
*.pyc
*.log
.pytest_cache/
.mypy_cache/
.ruff_cache/
.DS_Store
Dockerfile.dev
docker-compose*.yml
ビルド → 実行 → push
# 1) ビルド (CI がよく叩くコマンド)
docker build -t ghcr.io/curtis/myapp:1.0.0 -t ghcr.io/curtis/myapp:latest .

# 2) ローカル確認
docker run --rm -p 8000:8000 ghcr.io/curtis/myapp:1.0.0

# 3) 運用用デーモンモード + 永続データ + 自動再起動
docker run -d --name myapp \
  --restart unless-stopped \
  --network mynet \
  -p 127.0.0.1:8000:8000 \
  -v myapp-data:/app/data \
  -e DB_HOST=pg \
  ghcr.io/curtis/myapp:1.0.0

# 4) 別マシンで取得
docker pull ghcr.io/curtis/myapp:1.0.0

この流れが手に馴染めば — 一つのコンテナを定義 → ビルド → 運用 → デプロイまで一周回せます。これが Docker 基礎シリーズの到達点 です。

次へ — Docker 中級へ #

基礎シリーズは一つのコンテナをうまく作って動かすところまででした。次のシリーズは 複数コンテナ + より深いビルド にもう一歩入ります。扱う内容:

  • マルチステージビルド — ビルド依存とランタイム依存を分離してイメージスリミング
  • Docker Composeweb + db + cache を一つのファイルで定義 / 同時起動
  • healthcheck、depends_on、profiles — Compose の運用機能
  • ビルドキャッシュ深掘り — BuildKit、mount cachepip install / npm ci のキャッシュをビルド間で共有
  • ロギングとデバッグ — 複数コンテナのログを一箇所で見る
  • 環境変数と secrets — 秘密値をイメージに刻まずに扱うパターン

基礎で作った一つのコンテナの上に、運用環境へもう一歩近づく内容です。今シリーズで掴んだコマンドと概念 (レイヤー、キャッシュ、ボリューム、ネットワーク) の上にそのまま積み上がります。

まとめ #

基礎シリーズ 6 編を一行ずつまとめると:

  • #1 — コンテナ vs VM、Docker エコシステムの骨組み
  • #2FROM / RUN / COPY / CMD で最初の Dockerfile
  • #3build / run / ps / logs / exec / stop / rm の日常コマンド群
  • #4 — bind mount / named volume、ユーザー定義 bridge ネットワーク
  • #5 — Docker Hub / GHCR、tag / push / pull、ダイジェスト
  • #6 — .dockerignore、ビルドコンテキスト、レイヤーキャッシュ順序

Docker トラックは 4 シリーズです。次のシリーズは Docker 中級 — Compose とマルチステージビルドに入ります。

X