Docker: イメージ最適化とベストプラクティス

最終更新:2026-08-26

イメージの最適化は単なるサイズ削減ではなく、小さいイメージはデプロイの高速化、ストレージコストの削減、攻撃対象領域の縮小を意味します。

1. 学べること



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) 最適化前後の比較フレームワーク

100%
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 がイメージをビルドするとき、レイヤーの命令とコンテキストが変更されていなければキャッシュが再利用されます。あるレイヤーのキャッシュが期限切れになると、後続のすべてのレイヤーを再ビルドする必要があります。

100%
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-recommendsapt-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 パイプラインにスキャンステップを統合することをお勧めします。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度: ⭐): dive を使って既存のイメージ(例: nginx:latest)の各レイヤーを解析し、どのレイヤーが最も大きいか、どのファイルが追加されたかを記録してください。
  2. 応用問題 (難易度: ⭐⭐): hadolint を使ってレッスン 7〜9 の Dockerfile をチェックし、警告レベルの提案をすべて修正してください。
  3. 挑戦問題 (難易度: ⭐⭐⭐): alpine、debian、slim の 3 つのベースイメージでビルドした同じ Python アプリケーションのイメージサイズを比較し、サイズの違いの理由を分析してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%