Dockerfile最適化術:5つの秘訣でイメージサイズを80%削減する

午前3時。私は「Pushing to registry」と表示されたまま30分も動かない端末のプログレスバーを見つめていました。
3.2GB。
初めて作ったNode.jsアプリのDockerイメージは、ネットのチュートリアル通りに作ったはずなのに、信じられないほど肥大化していました。翌朝、同僚から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/*一見、最後でキャッシュを消しているので綺麗に見えます。しかし実際には、2行目でインストールされたキャッシュデータは「2層目」に永久保存されています。3行目は「ファイルが見えなくなった」という情報を記録しているだけで、データ自体はイメージの中に残ったままなのです。
これは、部屋の掃除をするたびに「掃除前の部屋の写真」を保存しているようなものです。ゴミを捨てても、ゴミが写った写真は残るので、アルバム(イメージ)の厚さは変わりません。
また、基礎イメージの選択も重要です。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.2GBから400MBまで減らすことができました。
Alpine Linuxとは?
コンテナのために設計された、セキュリティ重視の超軽量Linuxディストリビューションです。標準的なglibcの代わりにmusl libcを、aptの代わりにapkを使用します。
Alpineの互換性の罠
ただし注意点があります。musl libcを使用しているため、C/C++で書かれた一部のライブラリ(ネイティブ拡張)が動かないことがあります。以前、あるNode.jsの画像処理ライブラリが動かず苦労しました。
推奨戦略:
- 基本は Alpine版(
-alpine)を試す。 - 互換性エラーが出たら Slim版(
-slim、Debianベースの軽量版)を使う。 - それでもダメなら標準版(ごく稀です)。
# 変更前
FROM node:16
# 変更後
FROM node:16-alpineたったこれだけで800MB削減です。
テクニック2:RUNコマンドを結合する
「削除は同じ層で行う」。これを実現するために、関連するコマンドを && で繋いで、1つの RUN 命令にまとめます。
悪い例(3層作られる)
RUN apt-get update
RUN apt-get install -y python3 gcc
RUN rm -rf /var/lib/apt/lists/*これだと、2層目でキャッシュが保存され、3層目で消しても手遅れです。
良い例(1層で完結)
RUN apt-get update && \
apt-get install -y python3 gcc && \
rm -rf /var/lib/apt/lists/*これなら、インストールして掃除まで終わった状態だけが1つの層として保存されます。
バックスラッシュ \ の活用
長いコマンドは \ で改行して可読性を高めましょう。
何を結合すべき?
- 結合すべき:インストール+掃除、ダウンロード+解凍+削除。
- 結合しない:
npm installとnpm run buildなど、キャッシュを活用したい工程は分けた方がビルド時間が短縮できます。
私のプロジェクトでは、12個あったRUN命令を4個にまとめただけで、520MBから320MBまで縮みました。
テクニック3:マルチステージビルド(Multi-stage Build)
これぞ最強のダイエット術です。
GoやTypescript、C++などのコンパイルが必要な言語では、「ビルド環境」にはコンパイラやヘッダーファイルが必要ですが、「実行環境」にはコンパイル済みのバイナリだけあれば十分です。ビルド道具まで本番イメージに入れるのは無駄です。
マルチステージビルドを使えば、「ビルド用のイメージ」で作った成果物だけを、「実行用のイメージ」にコピーして持っていくことができます。
Node.js(TypeScript)の例
# === 第1ステージ:ビルド ===
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build # TypeScriptをJSにコンパイル
# === 第2ステージ:実行 ===
FROM node:16-alpine
WORKDIR /app
# ビルド済みのファイルだけコピー
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/index.js"]COPY --from=builder が魔法の言葉です。第1ステージの重たい中間ファイル(TSソースコード、キャッシュなど)は全て破棄され、綺麗な第2ステージだけがイメージとして残ります。
私の場合、TypeScriptのソースコードと開発依存パッケージ(devDependencies)を含めた400MBが、マルチステージビルドで220MBになりました。
さらに一歩進んで、実行ステージでは npm install --production で本番用パッケージだけ入れると、さらに軽くなります。
テクニック4:.dockerignore で無駄を排除
.dockerignore は .gitignore のDocker版です。
COPY . . を実行した時、指定したファイルを除外してくれます。これがないと、巨大な node_modules、.git ディレクトリ(履歴データ)、ローカルのログファイル、そして最悪の場合 .env(秘密鍵)までイメージにコピーされてしまいます。
プロジェクトルートに .dockerignore を作りましょう:
node_modules
npm-debug.log
.git
.gitignore
.env
.DS_Store
coverage/
dist/特に .git フォルダは見落としがちですが、長く続いているプロジェクトだと数百MBになっていることもあります。これを設定しただけで、ビルド時間が2分から30秒に短縮されました。
テクニック5:パッケージマネージャのキャッシュ削除
npm、pip、apt、apkなどは、次回インストールを速くするためにダウンロードしたファイルをキャッシュします。しかし、Dockerイメージにおいては「次」はないので、このキャッシュはただのゴミです。
各ツールごとの正しい掃除方法:
Alpine (apk)
RUN apk add --no-cache python3
# --no-cache オプションでそもそもキャッシュを作らせないDebian/Ubuntu (apt)
RUN apt-get update && apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*Node.js (npm)
RUN npm install && npm cache clean --forcePython (pip)
RUN pip install --no-cache-dir -r requirements.txt実測データでは、Pythonプジェクトで --no-cache-dir をつけるだけで140MBも軽くなりました。
究極の最適化ケーススタディ
最後に、私のNode.js APIサーバーがどう変化したか、全貌をお見せします。
初期状態(1.2GB)
FROM node:16
COPY . .
RUN npm install
CMD ["node", "index.js"]シンプルですが、Debian全入り+開発依存ファイル+キャッシュ+ソースコード全部入りです。
最適化適用後(180MB)
node:16-alpineに変更(-800MB).dockerignore追加(-20MB)- マルチステージビルド導入(-200MB)
- 本番依存のみインストール+キャッシュ削除(-40MB)
最終的なDockerfile
# Build Stage
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production Stage
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
# 本番依存のみ install し、キャッシュを掃除
RUN npm install --production && npm cache clean --force
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]結果:1.2GB → 180MB(85%削減)。
まとめ
Dockerイメージの最適化は、以下の5ステップで進めてください:
- Alpineを使う:まず土台を変えるのが一番効きます。
- マルチステージビルド:ビルド道具を実行環境に持ち込まない。
- RUNをまとめる:インストールと掃除はセットで。
- キャッシュを消す:
--no-cacheなどを活用。 - 余計なものを入れない:
.dockerignoreを忘れずに。
最初は面倒に感じるかもしれませんが、一度テンプレートを作ってしまえば、あとは使い回すだけです。イメージが軽くなれば、デプロイは速くなり、ストレージコストは下がり、セキュリティリスクも減ります(不要なソフトが入っていないため)。
ぜひ、あなたの手元のDockerfileを見直してみてください。「えっ、こんなに減るの?」という驚きが待っているはずです。
Dockerイメージ軽量化・完全フロー
5つのテクニックを組み合わせて、3.2GBのイメージを180MBまで94%削減する手順
⏱️ Estimated time: 1 hr
- 1
Step1: テクニック1:ベースイメージをAlpineに変更
FROM node:16 を FROM node:16-alpine に変更するだけで、約800MB削減できます。
注意点:Alpineはmusl libcを使用しているため、稀にC++ネイティブ拡張が動かないことがあります。その場合は -slim タグ(Debian精量版)を試してください。 - 2
Step2: テクニック2:RUNコマンドの結合と掃除
apt-get install と rm -rf /var/lib/apt/lists/* を && で繋ぎ、同じRUN命令内で実行します。
これにより、一時ファイルがレイヤーに残るのを防げます。
例:RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - 3
Step3: テクニック3:マルチステージビルド
ビルド環境(SDK、コンパイラ)と実行環境(ランタイム)を分けます。
AS builder でビルドし、COPY --from=builder で成果物だけを軽量な実行用イメージにコピーします。
これにより、ソースコードやビルドツールを最終イメージから排除できます。 - 4
Step4: テクニック4:不要ファイルの除外
.dockerignore ファイルを作成し、node_modules, .git, .env などを除外します。
ビルドコンテキストの転送時間が短縮され、意図しないファイルの混入を防げます。
FAQ
なぜRUNコマンドをまとめる必要があるのですか?
Alpineイメージを使ってエラーが出た場合は?
マルチステージビルドのメリットは何ですか?
npm installの最適化方法は?
4 min read · 公開日: 2025年12月17日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます