前回(Day 16)では、C# / ASP.NET Core Web API をコンテナ化するための「初めてのDockerfile構文(FROM, WORKDIR, COPY, RUN, ENTRYPOINT)」を学びました。
しかし、前回の単純なDockerfileには「イメージサイズが800MB超に肥大化してしまう」「本番環境にコンパイラやソースコードが丸見えで残ってしまう」という重大な課題がありました。
本日学ぶ『マルチステージビルド(Multi-stage builds)』は、この問題を完璧に解決し、現場のDockerイメージ作成において100%必須とされる最重要テクニックです!
開発環境と本番サーバーの分離を30年間実践してきたベテランエンジニアの皆様なら、その合理性に思わず膝を打つはずです。じっくりマスターしていきましょう!
① 導入・本日の達成目標
一般的なアプリケーション開発では、「ビルドに必要な道具(巨大なSDK、コンパイラ、開発用ヘッダー、NuGetキャッシュ)」と「本番で動かすために必要なもの(最小限のランタイムと発行済みDLL群)」は全く異なります。
もしビルド環境をそのまま本番コンテナに含めてしまうと、ディスク容量を圧迫するだけでなく、余計なツール類がサイバー攻撃の足場(攻撃対象領域: Attack Surface)となってしまいます。
本日の講義を終える頃には、以下の3つの目標を完全にクリアできるようになります。
- マルチステージビルドの概念と、1つのDockerfile内で複数の
FROMを使い分ける仕組みを論理的に理解できる。 - 約800MBのSDKイメージを約100MBのRuntimeイメージへ劇的にスリム化し、セキュリティを大幅に高める理由を説明できる。
COPY --fromを駆使して純粋な発行バイナリのみを抽出し、Dockerキャッシュを最大限に利かせる実践的なC# Dockerfileを自作できる。
② VM / Windowsサーバー技術者のためのパラダイムシフト
従来のWindows Server運用では、私たちは「ビルドサーバー」と「本番Webサーバー」を厳格に物理的・論理的に分離していました。
TFS(Team Foundation Server)やJenkins専用のビルドマシンにVisual Studioや巨大な.NET SDKをインストールしてコンパイルを行い、出力されたDLL群だけをZIPやファイル共有経由で本番のIISサーバーへと配置(XCOPY / Web Deploy)していました。
「本番サーバーにコンパイラや開発環境を入れてはならない」というのは、安定稼働とセキュリティにおける絶対の掟だったのです。
Dockerのマルチステージビルドは、この「ビルドサーバーと本番サーバーの分離」という大掛かりなインフラ構築を、たった1つのDockerfileの中でスマートに再現する仕組みです!
📊 旧来のビルド/本番分離 と Dockerマルチステージビルド の対比表
③ マルチステージビルドの3ステップと基本構文
マルチステージビルドの全体像は、以下の3つのステップで成り立っています。
Dockerfile内に FROM 命令を複数回書くと、それぞれの FROM は「全く新しい独立したステージ」を開始します。
重要なのは、「最終的に出力されるDockerイメージは、一番最後の FROM ステージの内容だけになる」という点です!
途中のステージ(ビルド用コンテナ)で生成された巨大なキャッシュや中間ファイルは、すべて自動的に使い捨て(破棄)されます。
🔑 構文の基本ルール:ステージに名前をつける(AS 句)
# ① 第1ステージ: 巨大な.NET SDKを使ってビルドを行う(ステージ名: build)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
... ビルド・発行コマンド ...
# ② 第2ステージ: 最小限のASP.NET Runtimeを使って本番イメージを作る(ステージ名: final)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# 第1ステージから成果物だけをコピー!
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyWebApi.dll"]
④ 核心:成果物のみを抽出する「COPY --from」の魔法
マルチステージビルドの最大の武器が、COPY --from=<ステージ名> 命令です。
通常の COPY 命令は「ホストPC(Windows側)のファイル」をコンテナへコピーしますが、--from=build を指定すると、「先行する build ステージのコンテナ内部のファイル」を直接参照して、現在のステージへコピーできます。
これにより、以下のような劇的なメリットが生まれます。
- ソースコードや.gitが本番に残らない: ソースファイル(.cs)やGit履歴はビルドステージにしか存在せず、本番イメージにはコンパイル済みのDLLと静的ファイルしか入りません。
- NuGetキャッシュ・objフォルダの完全排除: 膨大な中間生成物はビルドステージと共に破棄されます。
- 脆弱性(CVE)の激減: コンパイラやパッケージマネージャーなどの不要な実行可能バイナリが一切本番に入らないため、セキュリティスキャナ(Trivyなど)の検知件数が劇的に減少します。
⑤ 実践コード解説:ASP.NET Core 最適化Dockerfile完全版
それでは、現場で実際に利用されている「ビルドキャッシュを極限まで最適化したASP.NET Core Web APIのDockerfile完全版」を見ていきましょう。
# ========================================================
# ステージ1: ビルド環境(.NET 8 SDK / 約800MB)
# ========================================================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 【ポイント①】プロジェクトファイル(.csproj)だけを先にコピーして restore を実行
# ソースコードを変更しても、パッケージ追加がなければこの層はキャッシュされ一瞬で完了!
COPY ["MyWebApi.csproj", "./"]
RUN dotnet restore "MyWebApi.csproj"
# 【ポイント②】ソースコード全体をコピーして Release 発行
COPY . .
RUN dotnet publish "MyWebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false
# ========================================================
# ステージ2: 本番実行環境(ASP.NET Core 8 Runtime / 約100MB)
# ========================================================
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 8080
# 【ポイント③】build ステージの発行成果物だけをクリーンに転送
COPY --from=build /app/publish .
# コンテナ起動コマンド
ENTRYPOINT ["dotnet", "MyWebApi.dll"]
⚡ なぜ COPY を2回に分けるのか?(Dockerキャッシュの極意)
上記のコードで最も重要なテクニックが、「.csproj だけを先にCOPYして dotnet restore を行い、その後にソースコード全体をCOPYする」という2段階の記述です。
Dockerは各行(命令)ごとにキャッシュを作成します。もし最初から COPY . . としてしまうと、C#コードを1文字修正しただけでもキャッシュが無効化され、毎回重い dotnet restore(NuGetダウンロード)が再実行されてしまいます。
プロジェクトファイルを分離することで、「パッケージ構成が変わらない限り、依存ライブラリの復元処理を完全スキップ(0秒キャッシュヒット)」させることが可能になるのです!
⑥ Git連携とどこでも動くビルド(CI/CDの恩恵)
マルチステージビルドを導入すると、開発チームやCI/CDパイプラインに絶大な恩恵がもたらされます。
最も驚くべきメリットは、「開発者のPCやCIサーバーに .NET SDK が一切インストールされていなくても、Git cloneして docker build を叩くだけで完璧なビルドができる」という点です!
「AさんのPCは .NET 8.0.100、Bさんは 8.0.204、CIサーバーは 8.0.101 で、マイナーバージョンの違いによってビルドエラーが発生する」という不毛なトラブルを経験したことはありませんか?
マルチステージビルドなら、コンパイラやビルドツールのバージョンがDockerfile内で
sdk:8.0 として完全に固定されるため、地球上の誰がいつビルドしても寸分違わぬ同一成果物が保証されます!
⑦ ベテランがハマる落とし穴とベストプラクティス
罠① ソースコードをまとめて先にCOPYしてキャッシュを殺してしまう
前述の通り、COPY . . を最初に行ってしまうと、わずかなコード修正でも毎回NuGetパッケージのリストアが走り、ビルド時間が数分延びてしまいます。必ず「定義ファイル(.csproj)先行コピー ➡ dotnet restore」の順序を守りましょう。
罠② COPY --from のステージ名タイポ
FROM ... AS build と定義したステージ名は、COPY --from=build で大文字小文字を含めて正確に指定する必要があります。存在しないステージ名を指定すると「failed to solve: stage not found」というビルドエラーになります。
罠③ /p:UseAppHost=false の指定を忘れる
Linuxコンテナ向けのASP.NET Core発行では、/p:UseAppHost=false を指定してOS依存の実行ファイル(AppHost)を生成せず、純粋なクロスプラットフォームDLLを出力させるのが定石です。余計なファイルサイズを削り、移植性を高められます。
- マルチステージビルドは、巨大なビルド環境(SDK)と軽量な実行環境(Runtime)を1つのDockerfile内で多段階に分離する技術。
FROM ... AS buildでビルド作業を行い、COPY --from=buildで発行成果物だけを本番ステージへ安全に抽出する。- イメージサイズを 800MB超から約100MBへ劇的に削減し、ソースコードや不要ツールの混入を防いでセキュリティを極大化できる。
.csprojの先行コピーとdotnet restoreの分離により、Dockerレイヤーキャッシュを最大化して爆速ビルドを実現する。- ホストPCに .NET SDK が未インストールでも、Git cloneして
docker buildだけで完全動作するため、チーム開発とCI/CDの再現性が100%になる。
『.dockerignore の完全攻略』〜.git や bin/obj を除外してビルド転送量を劇的削減&ビルド爆速化〜
コメント
コメントを投稿