第18章:マルチステージビルドで“速く&軽く”する 🏗️🪶✨
今回の主役は マルチステージビルド! ざっくり言うと「ビルド用の重たい部屋」と「実行用のスリムな部屋」を分けて、最後は必要なものだけ持って帰る作戦です📦➡️🪶 Docker公式でも「最終イメージを小さくして、管理しやすくする」定番手法として紹介されています。(Docker Documentation)
1) 何が“速く”なるの?どこが“軽く”なるの?⚡️
マルチステージにすると、こういう得が出ます👇
- 軽くなる(=配布・起動が速い) 最終イメージに「TypeScriptコンパイラ」「テスト/ビルドツール」「開発用依存」などを入れないから、サイズが減ります。(Docker Documentation)
- 安全になりがち(=攻撃面・スキャン対象が減る) “本番で使わないツール”が最終イメージに残らないのは単純に強い💪(Docker Documentation)
- ビルドも効率化しやすい ステージを分けると、重たい処理(依存インストール・ビルド)のキャッシュ設計がしやすくなります。(Docker Documentation)
2) イメージ図で掴む(超重要)🧠🖼️
[builderステージ] 🏗️(重いけど必要)
- npm ci(dev依存も含む)
- tsc で build(dist 作る)
- テスト/生成など
↓ 必要な成果物だけ引っ越し 📦
[runnerステージ] 🪶(本番はここだけ)
- dist(実行に必要な成果物)
- 本番依存だけ(dependencies)
- 最小限の設定
ポイントは 「最終ステージ(runner)に“要る物だけ”」 です!(Docker Documentation)
3) まずは王道:TypeScript(Node)を2ステージ化してみよう 🐳📘
ここでは NodeのActive LTS系(例:v24系) をベースに書きます。Node公式のリリース表で v24 が Active LTS として掲載されています。(Node.js)
✅ Dockerfile(npm版・基本の型)
## syntax=docker/dockerfile:1
##########################
## 1) builder: ビルド専用🏗️
##########################
FROM node:24-slim AS builder
WORKDIR /app
## 依存だけ先に入れる(キャッシュが効きやすい)📦
COPY package.json package-lock.json ./
RUN npm ci
## ソースを入れてビルド🧱
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
##########################
## 2) runner: 実行専用🪶
##########################
FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
## 本番依存だけ入れる(devDependenciesは入れない)🚫
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
## 成果物だけ持ってくる📦✨
COPY --from=builder /app/dist ./dist
## よくある起動例(適宜変えてね)
CMD ["node", "dist/index.js"]
ここで何が嬉しいの?😆
builderにだけ tsc や devDependencies が居るrunnerは 本番依存 + dist だけ だから軽い🪶- しかもマルチステージは「ステージ間で成果物だけコピーできる」設計そのものが公式推奨。(Docker Documentation)
npm ciは “クリーンインストール”向けで、CI/デプロイのような場面に適していると npm 公式が説明しています。(npmドキュメント)--omit=dev(devDependenciesを除外)は npm 公式コマンド/説明として明記されています。(npmドキュメント)
4) 「速いのに壊れない」ための落とし穴ベスト5 😵💫🕳️
落とし穴1:node_modules を builder から runner にコピーしてしまう📦➡️💥
- やりがち:
COPY --from=builder /app/node_modules ./node_modules - これ、devDependenciesまで一緒に持ってきがちで太ります🐘
→ 今回の型みたいに runner で
npm ci --omit=devが安全寄り。
落とし穴2:builder と runner の土台OSを変えて “ネイティブ依存”で爆発💣
たとえば builder を Debian系、runner を Alpine にして、かつ node_modules をコピーすると、ネイティブアドオンが噛んで事故りやすいです😇
(どうしても Alpine を使いたいなら、Alpineは musl libc なのでハマりポイントがある、と公式イメージ側でも注意が書かれています)(hub.docker.com)
落とし穴3:最終イメージにソース(src)を入れてしまう📁
- 本番に必要なのが
distだけなら、srcは持ち込まないのが基本✂️ → マルチステージで自然に防げます。
落とし穴4:ステージ名を付けずに地獄になる👻
AS builder / AS runner は必須級!
公式も「ステージを分けて管理しやすく」って話の流れで、ステージ設計(命名・停止など)をまとめています。(Docker Documentation)
落とし穴5:ビルドのデバッグが難しい(でも解決できる)🔍
そんな時は 特定ステージで止めてビルドできます👇
--target で途中ステージだけ作れるのは公式ドキュメントにもあります。(Docker ドキュメント)
5) 🧪ミニ演習:自分のプロジェクトを“2ステージ化”して効果を測ろう📊
演習A:イメージサイズ比較してニヤける😏🪶
- いまのDockerfileでビルド
- この章のDockerfileにしてビルド
- それぞれサイズを見る
docker build -t myapp:single .
docker build -t myapp:multi -f Dockerfile .
docker image ls myapp:single myapp:multi
狙い:myapp:multi の方が軽くなってたら勝ち🏆✨
演習B:builderステージで止めて中身を見てみる👀
docker build --target builder -t myapp:builder .
docker run --rm -it myapp:builder bash
狙い:dist ができてるか、どこが重いか、builder内で確認できる🔎
(--target はマルチステージの公式機能)(Docker ドキュメント)
6) 🤖AI活用:マルチステージ化を“最短で当てる”プロンプト集 🧰✨
プロンプト1:あなたのDockerfileを2ステージに変換してもらう
以下のDockerfileを、TypeScript(Node)の本番向けマルチステージに変換してください。
条件:
- 最終イメージには dist と本番依存だけを含める
- npm ci を使い、runnerでは npm ci --omit=dev にする
- builder / runner を AS で命名
- 変更点を箇条書きで説明
Dockerfile:
(ここに貼る)
プロンプト2:キャッシュが効く順序になってるかレビューしてもらう
このDockerfileのキャッシュ効率をレビューしてください。
- 依存インストールがソース変更で毎回走らないか?
- COPYの順序は適切か?
- マルチステージで最終イメージに不要物が入ってないか?
改善案を3つ、理由付きで。
Dockerfile:
(ここに貼る)
7) 仕上げチェックリスト ✅🏁
-
builderとrunnerに分かれてる?🏗️🪶 -
runnerに TypeScript/テスト/ビルドツールが入ってない?🚫 -
runnerはdist+ 本番依存だけ?📦 -
--target builderでデバッグできる形?🔍(Docker ドキュメント) - Alpineを使うなら musl の注意点を理解してる?🧊(hub.docker.com)
次の第19章が「開発依存と本番依存を切り分ける」なので、ここで作った runnerで本番依存だけ入れる 型が、そのまま気持ちよく繋がりますよ😆🔗✨