Docker: イメージ最適化とベストプラクティス
最終更新:2026-08-26
イメージの最適化は単なるサイズ削減ではなく、小さいイメージはデプロイの高速化、ストレージコストの削減、攻撃対象領域の縮小を意味します。
1. 学べること
- イメージレイヤーの数最適化の戦略
- RUN コマンドの結合テクニック
- 依存関係のクリーンアップとキャッシュ管理
- Dockerfile Lint ツールの使用
- キャッシュ最適化手法の開発
2. CI エンジニアの実話
(1) 問題点: CI ビルド時間が 8 分から 25 分に急増
Bob の CI ビルド時間が突然 8 分から 25 分に急増しました。調査の結果、毎回すべての依存関係が再インストールされていることが判明しました — COPY . . が RUN pip install の前に置かれていたため、コード変更ごとに依存関係インストールキャッシュが期限切れになっていました。20 分の待ち時間が開発効率を大幅に低下させました。
(2) キャッシュ最適化のソリューション
Dockerfile 命令の順序を再配置し、変更頻度の低い COPY package.json を冒頭に移動することで、レイヤーキャッシュを活用してビルド時間を 3 分に短縮しました。
DOCKERFILE
# 修正前: コード変更で pip キャッシュが無効化される
COPY . .
RUN pip install -r requirements.txt
# 修正後: 依存関係の変更頻度 << コードの変更頻度
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
(3) メリット: ビルド時間が 25 分から 3 分に
わずか 2 行のコードの順序を入れ替えるだけで、ビルド時間は 25 分から 3 分(キャッシュヒット時)に短縮され、CI パイプラインの効率は 8 倍に向上しました。
3. イメージ最適化の主要戦略
(1) 最適化前後の比較フレームワーク
graph TB
subgraph Before["最適化前"]
B1["FROM ubuntu:22.04<br/>77 MB"] --> B2["RUN apt-get install<br/>300 MB"]
B2 --> B3["COPY . .<br/>(node_modules を含む)"]
B3 --> B4["RUN npm install<br/>200 MB"]
B4 --> B5["最終: 1.2 GB"]
end
subgraph After["最適化後"]
A1["FROM node:20-alpine<br/>135 MB"] --> A2["COPY package.json"]
A2 --> A3["RUN npm ci<br/>50 MB"]
A3 --> A4["COPY . .<br/>(node_modules を除く)"]
A4 --> A5["最終: 180 MB"]
end
(2) 最適化戦略の概要
| 戦略 | 効果 | 実装難易度 |
|---|---|---|
| ベースイメージを切り替え(alpine/slim) | サイズ ↓50〜80% | ⭐ |
| マルチステージビルド | 容量 ↓60〜90% | ⭐⭐ |
| RUN 併合 + キャッシュクリア | サイズ ↓20〜40% | ⭐ |
| COPY 順序の調整 | ビルド速度 ↑3〜8x | ⭐ |
| .dockerignore | コンテキスト ↓30〜90% | ⭐ |
| hadolint チェック | 潜在的な問題を発見 | ⭐⭐ |
4. RUN コマンドの併合
(1) 併合の原則
各 RUN コマンドは新しいレイヤーを作成します。関連する操作を 1 つの RUN コマンドにまとめ、同じレイヤー内で一時ファイルをクリーンアップします。
▶ サンプル: コマンドの併合 (難易度: ⭐⭐)
DOCKERFILE
# 悪い例: 3 つの個別レイヤー、apt キャッシュがイメージに残る
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y nginx
# 良い例: 1 レイヤー、同じレイヤー内でキャッシュをクリア
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
nginx && \
rm -rf /var/lib/apt/lists/*
(2) --no-install-recommends の効果
| オプション | インストール内容 | 典型的な節約サイズ |
|---|---|---|
| デフォルト | メインパッケージ + 推奨パッケージ + 提案パッケージ | ベースライン |
--no-install-recommends |
メインパッケージ + 依存関係のみ | ↓30〜50% |
5. キャッシュヒット率の最適化
(1) レイヤーキャッシュ機構
Docker がイメージをビルドするとき、レイヤーの命令とコンテキストが変更されていなければキャッシュが再利用されます。あるレイヤーのキャッシュが期限切れになると、後続のすべてのレイヤーを再ビルドする必要があります。
graph TB
L1["FROM python:3.12<br/>✅ キャッシュヒット"] --> L2["WORKDIR /app<br/>✅ キャッシュヒット"]
L2 --> L3["COPY requirements.txt<br/>✅ キャッシュヒット(変更なし)"]
L3 --> L4["RUN pip install<br/>✅ キャッシュヒット"]
L4 --> L5["COPY . .<br/>❌ キャッシュミス(コード変更)"]
L5 --> L6["CMD python app.py<br/>❌ 再ビルド"]
▶ サンプル: --no-キャッシュ ありなしのビルド比較 (難易度: ⭐⭐)
BASH
# 通常ビルド: 可能な箇所でキャッシュを使用
docker build -t myapp:1.0 .
# キャッシュなしで強制再ビルド
docker build --no-cache -t myapp:1.0 .
# 前のイメージをキャッシュソースとして使用(CI 最適化)
docker build --cache-from=myapp:latest -t myapp:1.0 .
(2) Dockerfile 命令の順序を最適化
DOCKERFILE
# ============================================
# 最適化された Dockerfile: 高いキャッシュヒット率
# ============================================
FROM node:20-alpine
WORKDIR /app
# 1. 最も変更頻度が低い: パッケージメタデータ
COPY package*.json ./
# 2. 依存関係インストール(package.json が変更されない限りキャッシュ)
RUN npm ci --only=production
# 3. 最も変更頻度が高い: アプリケーションコード
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
| シナリオの変更 | 最適化バージョン | 未最適化バージョン |
|---|---|---|
| コードのみ変更 | ✅ npm ci がキャッシュ使用(5 秒) | ❌ npm ci が再ビルド(3 分) |
| 依存関係バージョンを更新 | ✅ npm ci で再ビルド(必要) |
❌ npm ci で再ビルド(同上) |
| Dockerfile を変更 | ✅ 影響を受けたレイヤーのみ再ビルド | ❌ すべて再ビルド |
6. イメージ解析ツール
▶ サンプル: dive でイメージレイヤーを解析 (難易度: ⭐⭐)
dive はインタラクティブなイメージレイヤー解析ツールで、各レイヤーでどのファイルが追加、変更、削除されたかを表示します。
BASH
# dive をインストール(Linux)
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.deb
sudo dpkg -i dive_0.12.0_linux_amd64.deb
# イメージを解析
dive nginx:latest
💡 ヒント: dive のインターフェースは次を表示します: ① 左側に各レイヤーのサイズと効率スコア; ② 右側に各レイヤーに追加されたファイルツリー。赤色のマーキングは無駄なスペースを示します(後続のレイヤーで上書きまたは削除されたファイル)。
▶ サンプル: hadolint で Dockerfile をチェック (難易度: ⭐⭐)
hadolint は Dockerfile の Lint ツールで、ベストプラクティス違反をチェックします。
BASH
# hadolint をインストール(Linux)
wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint
# Dockerfile をチェック
hadolint Dockerfile
💻 出力:
TEXT
📖 参照専用
Dockerfile:3 DL3013 warning: Pin versions in pip. Instead of `pip install <package>` use `pip install <package>==<version>`
Dockerfile:5 DL3059 info: Do not use `--no-cache-dir` in pip install. It is redundant when `--cache-from` is used.
Dockerfile:7 DL3008 warning: Pin versions in apt-get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
7. 最適化前後の Dockerfile 完全比較
(1) Node.js アプリケーション最適化の例
DOCKERFILE
# ============================================
# 修正前: 未最適化 Dockerfile (1.2 GB)
# ============================================
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
DOCKERFILE
# ============================================
# 修正後: 最適化済み Dockerfile (180 MB)
# ============================================
# 1. フル Node ではなく Alpine ベース
FROM node:20-alpine
WORKDIR /app
# 2. 依存関係メタデータを最初にコピー(キャッシュ最適化)
COPY package*.json ./
# 3. 本番専用依存関係
RUN npm ci --only=production && \
npm cache clean --force
# 4. ソースコードをコピー(最も変更頻度が高い)
COPY . .
# 5. 非 root ユーザーで実行
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
(2) 最適化結果の比較
| 指標 | 最適化前 | 最適化後 | 改善 |
|---|---|---|---|
| イメージサイズ | 1.2 GB | 180 MB | 6.7x ↓ |
| ビルド時間(コード変更) | 3 分 | 5 秒 | 36x ↓ |
| ビルド時間(依存関係変更) | 3 分 | 45 秒 | 4x ↓ |
| セキュリティ脆弱性 | 200+ | 30 | 6.7x ↓ |
| CIS ベンチマークスコア | 45 | 85 | +40 |
8. 言語別インストールポリシー
| 言語 | 依存関係 | インストールコマンド | クリーンアップコマンド |
|---|---|---|---|
| Node.js | package*.json |
npm ci --only=production |
npm cache clean --force |
| Python | requirements.txt |
pip install --no-cache-dir |
(組み込みの --no-cache-dir) |
| Go | go.mod go.sum |
go mod download |
go clean -modcache |
| Java | pom.xml / build.gradle |
mvn dependency:resolve |
.m2 キャッシュをクリア |
| Ruby | Gemfile Gemfile.lock |
bundle install --without dev |
.bundle キャッシュをクリア |
9. 完全なサンプル: Node.js アプリケーションのエンドツーエンド最適化
DOCKERFILE
# ============================================
# 完全最適化された Node.js 本番用 Dockerfile
# 機能: alpine、マルチステージ、キャッシュ、非 root
# ============================================
# ---------- ステージ 1: ビルド ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---------- ステージ 2: 本番 ----------
FROM node:20-alpine
WORKDIR /app
# 本番用依存関係のみコピー
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# ビルド済みアプリケーションをビルダーからコピー
COPY --from=builder /app/dist ./dist
# セキュリティ: 非 root ユーザー
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
BASH
# ビルドして検証
docker build -t node-app:optimized .
# サイズを比較
docker images node-app
# dive で解析
dive node-app:optimized
# hadolint でチェック
hadolint Dockerfile
❓ よくある質問
Q 各 RUN はイメージサイズを増やしますか?
A はい、RUN がファイルを削除するだけの場合でもです — 新しいレイヤーは「削除操作」を記録しますが、下層レイヤーは依然として存在するからです。したがって、「インストール + クリーンアップ」は同じ RUN 内で完了させなければなりません。インストール中に作成された一時ファイルと削除操作が同じレイヤーに含まれ、下層レイヤーから一時ファイルが見えないようにするためです。
Q
apt-get clean はなぜイメージサイズを減らすのですか?A
apt-get clean だけではほぼ効果がありません(rm についても同様です)。同じ RUN 内で使用する必要があります: apt-get update && apt-get install && rm -rf /var/lib/apt/lists/*。--no-install-recommends は apt-get clean よりも効果的です。Q キャッシュがヒットしたかどうかをどうやって確認しますか?
A
docker build の出力で確認できます: Using cache はヒットを示し、Step 3/7 : RUN xxx は再ビルドを示します。--progress=plain を使って詳細なログを確認することもできます。CI では docker build --cache-from=app:latest を使って前のビルドのキャッシュを再利用できます。Q
--no-cache はいつ使うべきですか?A 2 つのシナリオで使います: ① ベースイメージにセキュリティアップデートが必要で再ビルドが必要な場合; ② ビルドが失敗し、キャッシュが破損していると思われる場合。日常的な開発では
--no-cache を使わないでください。毎回ビルドを最初から始めることになります。Q イメージのセキュリティ脆弱性をどうやって検出しますか?
A
docker scout quickview <イメージ>(公式 Docker ツール)または trivy image <イメージ>(オープンソース)。Trivy は最も人気のあるオープンソースイメージスキャンツールで、OS パッケージと言語依存関係の既知の CVE を検出できます。CI パイプラインにスキャンステップを統合することをお勧めします。📖 まとめ
- イメージ最適化の中核: レイヤー数の削減 + 同じレイヤー内でのクリーンアップ + より小さなベースイメージの選択
- RUN 併合の原則: 関連する操作を 1 つの RUN にまとめる; 同じレイヤー内でインストールとクリーンアップ
- キャッシュ最適化: 変更頻度の低い命令(COPY 依存関係ファイル)を冒頭に、変更頻度の高い命令(COPY ソースコード)を末尾に配置
--no-install-recommendsでインストールボリュームを 30〜50% 削減diveでイメージレイヤー効率を分析し、hadolintで Dockerfile のベストプラクティスをチェック- 完全な最適化プロセス: Alpine ベースイメージ → マルチステージビルド → キャッシュ最適化 → 非 root → ヘルスチェック
📝 練習問題
- 基本問題 (難易度: ⭐):
diveを使って既存のイメージ(例:nginx:latest)の各レイヤーを解析し、どのレイヤーが最も大きいか、どのファイルが追加されたかを記録してください。 - 応用問題 (難易度: ⭐⭐): hadolint を使ってレッスン 7〜9 の Dockerfile をチェックし、警告レベルの提案をすべて修正してください。
- 挑戦問題 (難易度: ⭐⭐⭐): alpine、debian、slim の 3 つのベースイメージでビルドした同じ Python アプリケーションのイメージサイズを比較し、サイズの違いの理由を分析してください。