はじめに
Dockerコンテナのイメージサイズを小さく保つことは、デプロイ時間の短縮、サーバーのストレージ節約、そして脆弱性を減らすセキュリティ向上(アタックサーフェスの削減)において非常に重要です。
しかし、単純にベースイメージを作成しようとすると、ビルドに必要なコンパイラ(gccなど)、パッケージマネージャ、あるいはソースコードのテストライブラリなどが最終的なコンテナイメージに混入し、イメージサイズが数百MB〜数GBに膨れ上がってしまいがちです。
この問題をエレガントに解決する機能が、Dockerの マルチステージビルド(Multi-stage builds) です。本記事では、マルチステージビルドの基本原則と、実際のNode.jsプロジェクトを例にした具体的な削減ステップを解説します。
1. マルチステージビルドとは?
マルチステージビルドとは、1つの Dockerfile 内で複数の FROM 指示子を使用し、ビルド処理をいくつかの「ステージ」に分ける手法です。
あるステージでビルドしたコンパイル済みの実行バイナリや依存ファイルを、次のステージへ「コピー」して引き継ぐことができます。これにより、ビルド用ステージで使った巨大な開発用パッケージ(コンパイラ、SDK、ソースファイルなど)をすべて切り捨て、実行に必要な最小限のファイルだけを本番用ステージに残すことができます。
【ビルドステージ】
・ベースイメージ: node (大)
・開発ライブラリ、ソースコード
・ビルドコマンド実行 (npm run build)
│
▼ (必要な成果物だけをコピー)
【本番ステージ】
・ベースイメージ: alpine / scratch (極小)
・ビルド成果物 (dist/ や html)のみ
・実行コマンド
2. 従来型とマルチステージビルドの比較
実際に、Node.js(TypeScript)アプリケーションをコンテナ化する例で、従来型とマルチステージ型を比較してみましょう。
避けるべき従来型の Dockerfile
# 開発・ビルドに必要なツールが含まれる重いイメージ
FROM node:20
WORKDIR /app
# 依存パッケージのインストール
COPY package*.json ./
RUN npm install
# ソースコードのコピーとビルド(TypeScript -> JS)
COPY . .
RUN npm run build
# アプリケーションの起動
EXPOSE 3000
CMD ["node", "dist/index.js"]
- 課題: このイメージには、ビルド後に不要になるTypeScriptコンパイラ(
devDependencies)、ソースファイル(.tsファイル)、そしてnode_modules内の膨大な開発用ツールキットがすべて含まれたままになります。結果として、イメージサイズは簡単に 800MB〜1GB を超えてしまいます。
改善されたマルチステージビルドの Dockerfile
# -----------------------------------------------
# ステージ1: ビルド用環境 (builder)
# -----------------------------------------------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
# 開発用ライブラリも含めてすべてインストール
RUN npm ci
COPY . .
# TypeScriptをJavaScriptにコンパイル
RUN npm run build
# 本番環境用に、不要なdevDependenciesを削除して再インストール
RUN npm prune --production
# -----------------------------------------------
# ステージ2: 本番実行用環境 (runner)
# -----------------------------------------------
# 実行に必要な最小限の軽量イメージ (alpine)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# ステージ1 (builder) からビルド成果物と本番用のnode_modulesだけをコピー
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
3. なぜ劇的にサイズが減るのか?(ビルド結果)
このマルチステージビルドを適用することで、以下のような効果が得られます。
- 開発依存(devDependencies)の完全排除:
npm prune --productionにより、TypeScriptなどのビルド時にしか使わない重いツール群が除外され、本番用モジュールだけが残ります。 - alpineベースイメージの採用: Debianベースの重い node イメージの代わりに、Alpine Linuxベースの軽量イメージを使用することで、ベースOSの容量を削減します。
- 結果: イメージサイズが約 900MB から、なんと 150MB以下 まで激減します。
4. さらにサイズを下げるためのテクニック
.dockerignore ファイルの作成
Dockerfileと同じ階層に .dockerignore ファイルを作成し、ビルドコンテキストに不要なファイル(ローカルの node_modules や .git、環境変数ファイル .env など)が含まれないようにします。
# .dockerignore の例
node_modules
.git
.github
npm-debug.log
dist
.env
キャッシュの最適化
COPY package*.json ./ をソースコードのコピー(COPY . .)より前に行うことで、ソースコードに変更があっても package.json が変わっていなければ npm install のキャッシュ層が再利用され、ビルド時間が短縮されます。
まとめ
Dockerのマルチステージビルドは、コンテナ運用の効率を最大化するための必須テクニックです。
AS builderのようにビルドステージに名前をつけて処理を実行するCOPY --from=builderで実行に必要な最小限のファイルのみを本番ステージに引き継ぐ- OSのベースには軽量な
alpineやdistrolessイメージを選ぶ
デプロイパイプラインの高速化と安全性の向上のために、ぜひマルチステージビルドを導入してみてください。
