Dockerfile 最適化実践:5 つのテクニックでイメージを 80% スリム化
ターミナルのプログレスバーが「Pushing to registry」のまま、30 分止まったまま。
3.2GB。
初めて作った Node.js アプリの Docker イメージは、ネットのチュートリアル通りに Dockerfile を書いたはずなのに、想定外のサイズに膨らんでいました。翌朝、同僚から Slack で「そのイメージ、OS 丸ごとパッケージしてない? ノート PC のディスクがもう一杯なんだけど」と突っ込まれました。
Ubuntu のベースイメージ? node_modules? それともビルドツール? 原因はともかく、シンプルな API サービスなのに、イメージがプロジェクトコードの 50 倍も大きい。Docker 公式ドキュメントとベストプラクティスを調べ、この 3.2GB の怪物を 180MB まで落としました。94% 削減です。
この記事では、軽量化の過程で特に効いた 5 つのテクニックを紹介します。手順だけでなく「なぜ効くのか」も説明します。コマンドの暗記より、原理の理解のほうが応用に効きます。
先に理解する:Docker イメージが巨大になる理由
テクニックの前に、問題の根源を押さえましょう。
Docker イメージはレイヤー構造です。RUN、COPY、ADD のたびに新しいファイルシステム層が積み重なります。重要なのは、各層は追加されるだけで、中身は削除されない という点です。
たとえば Dockerfile にこう書いたとします:
RUN apt-get update
RUN apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*
一見、最後の行で apt キャッシュを消しています。しかしキャッシュはすでに第 2 層に永久保存されています。第 3 層は「これらのファイルは削除された」という印だけで、データはイメージ内に残ったままです。
部屋の整理のたびに「ゴミがある状態」の写真を撮り、最後にゴミを捨てても、ゴミ入りの写真はアルバム(イメージ)に残る——少しおかしいですが、Docker の CoW(写時コピー)はそう動きます。
未最適化のイメージで docker history を見ると、層の合計サイズが実際に必要なファイルをはるかに上回ることがよくあります。
見落としがちなのがベースイメージです。ubuntu:20.04 だけで 72MB、node:16 は 1.09GB——Debian フルセットと、使わないかもしれないシステムツールが入っています。
整理すると、最適化の方針は 3 つです。レイヤー数を減らす、軽いベースイメージを選ぶ、インストールと掃除を同じ層で完結させる。
テクニック 1:ベースイメージ選びでスタートダッシュ
ベースイメージは、家づくりの立地選びに近いです。最初の選択がサイズの下限を決めます。
数字で比較すると:
node:16→ 1.09GBnode:16-slim→ 240MBnode:16-alpine→ 174MBalpine:latest→ 5.6MB
差は歴然です。node:16 から node:16-alpine に変えただけで、コードを 1 行も変えず 1.2GB から 400MB まで落ちました。
Alpine Linux とは?
コンテナ向けに設計された Linux ディストリビューションです。極小構成で、musl libc と apk パッケージマネージャを使います。
利点は明確です:
- 体積が小さい(5MB vs Ubuntu 72MB)
- セキュリティ(攻撃面の最小化)
- 起動が速い
ただし落とし穴もあります。
Alpine の互換性トラップ
musl libc のため、一部のプリビルドバイナリが動きません。C++ 製の Node.js ネイティブモジュールで「library not found」が出たことがあります。原因は libc の違いでした。
現実的な方針は次のとおりです:
- まず Alpine 系(
-alpine)を試す - 互換性問題があれば
-slim(Debian 精量版) - それでもダメなら標準イメージ(かなり稀)
変更は 1 行です:
# 最適化前
FROM node:16
# 最適化後
FROM node:16-alpine
これだけで約 800MB 節約できます。
効果の確認
ビルド後に実行:
docker images your-image-name
SIZE 列を確認。まだ大きければ、ベースイメージ以外にも原因があります。
テクニック 2:RUN 命令を統合してレイヤーを減らす
理解は簡単ですが、実装で見落とされがちです。
各 RUN が 1 層を作ります。削除は同じ層の中でしか効きません。
悪い例:
# 誤った例(3 層)
RUN apt-get update
RUN apt-get install -y python3 gcc
RUN rm -rf /var/lib/apt/lists/*
apt キャッシュ(数十 MB)が第 2 層に残り、第 3 層の削除は「見えなくする」だけです。
正しい例:
# 正しい例(1 層)
RUN apt-get update && \
apt-get install -y python3 gcc && \
rm -rf /var/lib/apt/lists/*
インストールと掃除が同じ層で完結し、削除が実削除になります。
バックスラッシュ \ の役割
長いコマンドを複数行に分け、読みやすくします。
何を統合すべきか
すべてをまとめる必要はありません。目安は次のとおりです。
- 統合する:インストール + 掃除、ダウンロード + 解凍 + 圧縮ファイル削除
- 分ける:論理的に無関係な処理、よく変わるステップ(ビルドキャッシュを壊すため)
例:
# 良い層分け
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN npm install
RUN npm run build
システム依存は 1 層、package.json が変わる npm install は別層、ビルドも別層。package.json 変更時に前半がキャッシュされます。
実測では、12 個の RUN を 4 個にまとめ、520MB から 320MB まで縮みました。
テクニック 3:マルチステージビルド——必要なものだけ持っていく
マルチステージビルド(Multi-stage Build)は、Docker イメージを瘦せさせる最強の手段です。
考え方はシンプル。ビルドと実行を分ける。
Go でコンパイルするにはツールチェーンが数百 MB 必要ですが、バイナリは 10MB かもしれません。ツールチェーンまで本番イメージに入れるのは無駄です。
1 つの Dockerfile で複数ステージを定義し、第 1 ステージでビルド、第 2 ステージには成果物だけコピーします。
Node.js の例:
# === ビルドステージ ===
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# === 実行ステージ ===
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
COPY --from=builder がポイントです。第 1 ステージの中間成果物は捨てられ、第 2 ステージだけが最終イメージになります。
マルチステージを使う場面
- コンパイル言語:Go、Rust、C++ など
- フロントエンド:TypeScript コンパイル、Webpack バンドル
- ビルドツールが必要な Python:gcc でネイティブ拡張をビルドするケースなど
TypeScript を JavaScript にするプロジェクトでは、ソース + node_modules の @types で 400MB、dist は 2MB。マルチステージで 220MB まで落ちました。
よくある落とし穴
実行ステージでも npm install して「依存は必要だから」と全部入れるのは避けてください。devDependencies まで入り、無駄に肥大化します。
正しくは、ビルドステージで npm install(コンパイル用に dev 依存込み)し、node_modules をコピーするか、より精密には:
# ビルドステージ
RUN npm install
# 実行ステージ
RUN npm install --production
本番依存だけにすると、さらに 30〜40% 軽くなります。
最初は少し複雑に感じますが、理解すると設計の美しさがわかります。旅行の荷造り——家(ビルド)では全部広げて整理し、飛行機(実行)にはスーツケースの必需品だけ。
テクニック 4:.dockerignore で不要ファイルを除外
.dockerignore は .gitignore の Docker 版ですが、見落とされがちです。
COPY . . では、ディレクトリ全体がビルドコンテキストとして daemon に送られます。数百 MB の node_modules、.git 履歴、テスト、ログまで送られると、ビルドが遅く、.env のような秘密まで混入する危険があります。
プロジェクトルートに .dockerignore を置きます。
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
.vscode
.idea
*.md
.DS_Store
coverage/
.pytest_cache/
__pycache__/
*.pyc
dist-local/
入れるべきもの
- 既存の依存ディレクトリ:node_modules、vendor、target など(ビルドで再インストールする)
- 開発ツール設定:.vscode、.idea、.editorconfig
- Git 関連:.git、.gitignore(.git は数十〜数百 MB になり得る)
- ドキュメント:README、CHANGELOG、docs/
- 機密情報:.env、credentials.json、*.pem
.git を入れ忘れたプロジェクトでは、毎回 500MB の履歴を転送していました。.dockerignore 追加後、ビルドは 2 分から 30 秒、イメージも小さくなりました。
実用テクニック
何がコピーされたか不安なら、一度ビルドしてコンテナ内を確認:
docker run --rm -it your-image sh
ls -lah
不要なファイルがあれば .dockerignore に追加します。
シンプルに見えますが、特にフロントエンドでは dist、node_modules、.cache だけで GB 級になることもあります。
テクニック 5:パッケージマネージャのキャッシュを削除
npm、pip、apt、apk はインストール後にキャッシュを残します。ローカル開発では次回が速くなりますが、Docker イメージではただのデッドウェイトです。
同じ RUN 内で削除する
もう一度強調します。削除はインストールと同じ層で。
# ❌ 効かない削除
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # ほぼ無意味
# ✅ 効く削除
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
ツール別の掃除方法:
Node.js (npm/yarn)
# npm - 従来
RUN npm install && \
npm cache clean --force
# npm - シンプル(キャッシュ無効)
RUN npm install --no-cache
# yarn
RUN yarn install && \
yarn cache clean
Python (pip)
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install -r requirements.txt && \
rm -rf ~/.cache/pip
Alpine (apk)
RUN apk add --no-cache package-name
RUN apk add package-name && \
rm -rf /var/cache/apk/*
Debian/Ubuntu (apt)
RUN apt-get update && \
apt-get install -y package-name && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
実測
同じ Python 依存で:
- キャッシュ削除なし:450MB
- 削除あり:320MB
--no-cache-dir:310MB(最もクリーン)
140MB の差。オプション 1 つです。
開発 vs 本番
本番イメージでは --production や --no-dev で必須パッケージだけに。dev 依存は全体の 30〜50% を占めることもあります。
RUN npm install --production
RUN pip install --no-cache-dir -r requirements-prod.txt
5 テクニックを組み合わせると、効果は足し算ではなく掛け算に近いです。3.2GB を 180MB にしたのも、すべてを適用した結果です。
完全事例:Node.js アプリの最適化の全過程
理論だけでなく、Express API サービスの Dockerfile がどう変わったかを追います。
最適化前(1.2GB)
FROM node:16
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
単純ですが、イメージは巨大です。
第 1 段階:Alpine ベース(→ 400MB、-67%)
FROM node:16-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
1 行変更で 800MB 節約。
第 2 段階:.dockerignore(→ 380MB、-5%)
node_modules
.git
*.md
.env
coverage
見た目の削減は小さくても、ビルド速度は大きく改善。
第 3 段階:マルチステージ(→ 220MB、-42%)
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
ビルド中間ファイルを実行イメージから排除。効果最大のステップです。
第 4 段階:本番依存のみ + キャッシュ削除(→ 180MB、-18%)
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production --no-cache && \
npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
最終版 180MB。初期 1.2GB から 85% 削減。
最適化ロードマップ
1.2GB (node:16 原版)
↓ Alpine に変更
400MB (-67%)
↓ .dockerignore
380MB (-5%)
↓ マルチステージ
220MB (-42%)
↓ 本番依存 + キャッシュ削除
180MB (-18%)
────────────────
合計 85% 削減
どのテクニックが効きやすいか
- Alpine:即効性が高く、最初に試すべき
- マルチステージ:効果最大だが学習コストあり
- 本番依存 + キャッシュ削除:細部の積み上げ
時間が限られていれば、まず 1 と 2 を。
まとめ
5 つのテクニックを振り返ります:
- Alpine ベースイメージ — 源流から小さくする
- RUN の統合 — 同じ層でインストールと掃除
- マルチステージビルド — 実行に必要なものだけ残す
- .dockerignore — 不要・機密ファイルを除外
- キャッシュ削除 —
--no-cache系オプションを活用
組み合わせが前提です。Alpine + マルチステージで 80% は解決し、残りは掃除と除外で詰められます。
今すぐ試す
問題が出てからではなく、既存プロジェクトで 5 つを試してください:
- Alpine に替えられるか確認(多くは可能)
- 分離したインストールと削除がないか見る(あれば統合)
- マルチステージを追加(コンパイル系は必須)
.dockerignoreを作る- パッケージマネージャに
--no-cacheを付ける
ビルド後に docker images で Before/After を比較してください。
さらに深く
- Docker BuildKit のキャッシュマウント
- Distroless イメージ(Google 製、Alpine より小さいことも)
- セキュリティスキャン(Trivy、Grype)
Dockerfile 最適化は一度きりではなく、ビルドのたびにサイズを見る習慣が効きます。
イメージがどんどん軽くなることを願っています。
Dockerfile 最適化の完全フロー
5 つのテクニックでイメージを 80% スリム化。3.2GB を 180MB へ、94% 削減
⏱️ 目安時間: 1 時間
- 1
ステップ1: テクニック 1:Alpine ベースイメージを使う
Alpine ベースイメージの利点:
• 体積が小さい(5MB のみ。Ubuntu は 200MB)
• セキュリティが高い(攻撃面を最小化)
• パッケージ管理がシンプル(apk)
• 本番環境に向く
Alpine の使い方:
• FROM ubuntu:20.04 を FROM alpine:latest に変更
• apt-get の代わりに apk でインストール:apk add --no-cache nodejs npm
• イメージを 200MB から 5MB へ、95% 削減 - 2
ステップ2: テクニック 2〜3:RUN の統合とマルチステージビルド
RUN 命令の統合:
• 複数の RUN を 1 つにまとめ、レイヤー数を減らす
• && でコマンドを連結し、\ で改行して可読性を保つ
• 最後にキャッシュを掃除:
RUN apt-get update && \
apt-get install -y nodejs npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
マルチステージビルド:
• 第 1 ステージでフルビルドイメージを使ってコンパイル
• 第 2 ステージで最小の実行イメージだけを使う
• 実行時に必要なファイルだけ残し、ビルドツールは含めない
• イメージサイズが大幅に削減される - 3
ステップ3: テクニック 4〜5:.dockerignore とキャッシュ削除
.dockerignore の設定:
• node_modules、.git、.env、dist など不要ファイルを除外
• ビルドコンテキストを小さくし、ビルドを高速化
キャッシュ削除:
• --no-cache オプション:apk add --no-cache
• apt キャッシュ削除:apt-get clean && rm -rf /var/lib/apt/lists/*
• npm キャッシュ削除:npm cache clean --force
• イメージサイズを削減
FAQ
Dockerfile 最適化の 5 つのテクニックは何ですか?
1) Alpine ベースイメージ(Ubuntu 200MB から Alpine 5MB へ、95% 削減)
2) RUN 命令の統合(レイヤー数を減らしイメージを軽量化)
3) マルチステージビルド(実行に必要なファイルだけ残す)
4) .dockerignore の設定(node_modules、.git などを除外)
5) キャッシュ削除(apt-get clean、npm cache clean など)
最適化効果:
• Node.js アプリのイメージを 3.2GB から 180MB へ(94% 削減)
• 1.2GB から 180MB へ(85% 削減)
• デプロイ時間を 30 分から数分へ短縮
• イメージサイズが大幅に削減
なぜ Alpine ベースイメージが優れているのですか?
• 体積が小さい(5MB のみ。Ubuntu は 200MB)
• セキュリティが高い(攻撃面を最小化)
• パッケージ管理がシンプル(apk)
• 本番環境に向く
Alpine の使い方:
• FROM ubuntu:20.04 を FROM alpine:latest に変更
• apt-get の代わりに apk:apk add --no-cache nodejs npm
• イメージを 200MB から 5MB へ、95% 削減
RUN 命令はどう統合しますか?
• 複数の RUN を 1 つにまとめ、レイヤー数を減らす
• && でコマンドを連結
• \ で改行して可読性を保つ
• 最後にキャッシュを掃除:
RUN apt-get update && \
apt-get install -y nodejs npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
レイヤー数を減らし、イメージを軽量化し、ビルド効率も上がります。
Dockerfile 最適化のベストプラクティスは何ですか?
• Alpine ベースイメージを使う
• RUN 命令を統合する
• マルチステージビルドを使う
• .dockerignore を設定する
• キャッシュを削除する
• ベースイメージのバージョンを定期的に更新する
これらは単独ではなく組み合わせるのが効果的です。経験上、Alpine + マルチステージビルドで 80% のサイズ問題は解決し、残り 20% は掃除と除外で詰められます。
Dockerfile 最適化は一度きりではなく継続的な改善です。ビルドのたびにイメージサイズを確認する習慣をつければ、自然とコントロールできます。
4分で読めます · 公開日: 2025年12月17日 · 更新日: 2026年6月8日
Docker 実践ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Docker Compose 本番デプロイの三要素:ヘルスチェック、再起動ポリシー、リソース制限
Docker Compose 本番デプロイの三要素を詳しく解説。サービスの準備完了を判断するヘルスチェック、自動復旧を実現する再起動ポリシー、暴走を防ぐリソース制限。完全な YAML 設定テンプレート付きで、安定したコンテナ化アプリケーションを構築できます。
第 7 / 37 記事
次の記事
Docker ログ管理の実践:ドライバー設定から集中収集まで
Docker ログドライバーの種類、ローテーション設定、集中収集の設計を解説。本番環境のベストプラクティスとよくある落とし穴への対処法をまとめました。
第 9 / 37 記事
関連記事
Dockerfile入門:ゼロから最初の Docker イメージを作る(実例付き)
Dockerfile入門:ゼロから最初の Docker イメージを作る(実例付き)
Docker vs 仮想マシン:5分で理解する性能差とシーン別選び方ガイド
Docker vs 仮想マシン:5分で理解する性能差とシーン別選び方ガイド
Docker インストールの落とし穴ガイド 2025:permission denied から正常起動までの完全解決策
コメント
GitHubアカウントでログインしてコメントできます