Start Debugging

.NET 11 のコンテナーイメージにおける framework-dependent と self-contained と Native AOT の比較

ASP.NET Core サービスを .NET 11 で動かすなら、chiseled な aspnet イメージ上の framework-dependent が正しい既定です。ランタイムのレイヤーがサービス間で共有され、ランタイムの CVE はベースイメージの差し替えだけで塞げるからです。self-contained + トリミングと Native AOT はイメージを 2 倍から 5 倍小さくし、コールドスタートを大幅に速くしますが、その代わりに前述の利点を失います。実測された公開サイズ、共有レイヤーの計算、そして AOT の経路を壊す .NET 11 のベースイメージ推論バグを扱います。

.NET 11 上の一般的な長時間稼働の ASP.NET Core サービスであれば、chiseled な aspnet イメージ上に framework-dependent で発行してください。実際に出荷するものとしては最小であり (他のサービスがすでに取得済みのランタイムレイヤーの上に、数メガバイトのアプリケーションが乗るだけです)、ランタイムの CVE はアプリケーションを作り直してテストし直して再デプロイするのではなく、新しいベースイメージタグで再ビルドするだけで塞げます。アプリケーションが特定のランタイムパッチに固定される必要がある場合や、.NET がまったく入っていないベースイメージ上で動かす必要がある場合は、self-contained + トリミングに切り替えてください。Native AOT に手を伸ばすのは、コールドスタートまたは Pod あたりのメモリーが支配的な制約であり、かつ依存関係ツリー全体に対して dotnet publish が AOT 警告をひとつも出さない場合だけにしてください。AOT について語られるサイズの数字は本物ですが、フリートで見ると測っている対象が違います。framework-dependent なイメージはノード上のすべてのサービスでひとつのランタイムレイヤーを共有しますが、self-contained と AOT のイメージは共有しません。

ここでの内容はすべて <TargetFramework>net11.0</TargetFramework> を対象にしています。執筆時点で .NET 11 は Preview 7 (11.0.100-preview.7.26381.103、2026-08-11 リリース) であり、正式版は 2026 年 11 月が予定されています。プレビューのイメージタグには正式版で外れる -preview 修飾子が付くため、今日の 11.0-preview-resolute-chiseled は 11 月には 11.0-resolute-chiseled になります。以下の仕組みは .NET 8 以降安定しているので、ほぼすべてが .NET 9 と .NET 10 にもそのまま当てはまります。

コンテナーイメージとしての 3 つのモード

項目framework-dependentself-contained + トリミングNative AOT
ベースイメージのリポジトリdotnet/aspnet または dotnet/runtimedotnet/runtime-depsdotnet/runtime-deps
ランタイムの置き場所ベースイメージのレイヤーアプリケーションのレイヤーバイナリー内にコンパイル済み
ランタイムレイヤーがサービス間で共有されるはいいいえいいえ
ランタイムの CVE への対応新しいベースタグを取得して再ビルド新しい SDK、再ビルド、再テスト、再デプロイ新しい SDK、再ビルド、再テスト、再デプロイ
インストール済みパッチへのロールフォワードはいいいえいいえ
有効化する方法何もしない (既定)--self-contained -p:PublishTrimmed=true-p:PublishAot=true
RID が必要かいいえはいはい
ビルドホストに C ツールチェーンが必要かいいえいいえはい (clang、zlib1g-dev)
リフレクション、Reflection.Emit、プラグイン読み込み完全に使えるトリミング警告、実行時の失敗の可能性制限あり、または利用不可
サンプルイメージ、圧縮後52.81 MB21.86 MB11.60 MB

最後の 3 つの数字は dotnet/dotnet-docker にある .NET コンテナーイメージのサイズレポート からのもので、releasesapi サンプルを .NET 10.0 と noble-chiseled ベースイメージで測定した値です。詳細はすぐ後で説明します。この行こそが誤解を生むからです。

各モードが実際にイメージへ入れるもの

SDK のコンテナーツールはプロジェクトからベースイメージを推論します。規則は短いものです。コンテナー化のリファレンスによれば、self-contained なプロジェクトには mcr.microsoft.com/dotnet/runtime-deps が、ASP.NET Core プロジェクトには mcr.microsoft.com/dotnet/aspnet が、それ以外には mcr.microsoft.com/dotnet/runtime が使われます。タグは TFM の数値部分で、ContainerFamily がサフィックスとして付きます。

この推論がすべてです。

3 つとも Microsoft イメージの同じ堅牢化を継承します。UID 1654 の非 root ユーザー app ($APP_UID で公開) と、80 ではなく 8080 のポートで、いずれも .NET 8 で導入されました。chiseled イメージにはさらにシェルもパッケージマネージャーも curl も入っていないので、chiseled ファミリーを選んだ場合は 3 つのモードのいずれでも docker exec によるデバッグやシェルベースのヘルスチェックは動きません。

3 つそれぞれの発行方法

framework-dependent。RID は不要で、chiseled な ASP.NET Core ベースへ直接発行します。

# .NET 11 SDK 11.0.100-preview.7. Framework-dependent onto aspnet:11.0-preview-resolute-chiseled.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  -p ContainerFamily=resolute-chiseled \
  -p ContainerRepository=orders-api

self-contained + トリミング。PublishTrimmedSelfContained を含意しますが、後で読む人がそれを覚えていなくて済むように両方書いておきます。

# .NET 11 SDK 11.0.100-preview.7. Self-contained + trimmed onto runtime-deps:11.0-preview-resolute-chiseled.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  --self-contained \
  -p PublishTrimmed=true \
  -p ContainerFamily=resolute-chiseled \
  -p ContainerRepository=orders-api

Native AOT。PublishAot は self-contained を含意し、ビルドマシンにプラットフォームの C ツールチェーンが必要です。

# .NET 11 SDK 11.0.100-preview.7. Native AOT onto runtime-deps:11.0-preview-resolute-chiseled.
# Requires clang and zlib1g-dev locally, or build inside sdk:11.0-preview-aot.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  -p PublishAot=true \
  -p ContainerFamily=resolute-chiseled \
  -p ContainerRepository=orders-api

エージェントに clang を入れずに CI から実行したい場合、SDK の AOT イメージはまさにそのために存在します。

# .NET 11 preview. Multi-stage AOT build.
FROM mcr.microsoft.com/dotnet/sdk:11.0-preview-resolute-aot AS build
WORKDIR /src
COPY . .
RUN dotnet publish OrdersApi/OrdersApi.csproj -c Release -r linux-x64 -p:PublishAot=true -o /app

FROM mcr.microsoft.com/dotnet/runtime-deps:11.0-preview-resolute-chiseled
WORKDIR /app
COPY --from=build /app/OrdersApi .
USER $APP_UID
ENTRYPOINT ["./OrdersApi"]

Container* プロパティーの全体像、タグの制御、レジストリー認証については、Dockerfile なしで .NET 11 アプリケーションをコンテナーイメージとして発行する方法を参照してください。

公開されているサイズの数字

Microsoft はサンプルの最小 web API について、すべてのベースイメージバリアントでの実測サイズを公開しているので、推測する必要はありません。以下は .NET 10.0 における releasesapi サンプルの圧縮後サイズです。

ベースイメージframework-dependentself-contained + トリミングNative AOT
フル Ubuntu (10.0)92.48 MB61.53 MB51.27 MB
10.0-noble-chiseled52.81 MB21.86 MB11.60 MB
10.0-noble-chiseled-extra67.68 MB36.82 MB26.56 MB
10.0-alpine51.93 MB20.95 MB10.69 MB
10.0-alpine-extra66.50 MB35.52 MB25.25 MB

この表からすぐに 2 つのことが読み取れます。第 1 に、ベースイメージのファミリーはデプロイモードよりも大きなレバーです。framework-dependent なアプリケーションをフル Ubuntu イメージから noble-chiseled に移すと 39.67 MB 減りますが、これは同じアプリケーションをフルイメージ上で framework-dependent から Native AOT に切り替えて得られる削減 (41.21 MB) に匹敵し、しかも互換性の作業を一切必要としません。まだ chiseled にしていないなら、まずそれをやって測り直してから、他を検討してください。

第 2 に、chiseled の Native AOT は chiseled の framework-dependent より確かにおよそ 4.5 倍小さくなります。これは本物の利点で、scale-to-zero の関数や非常に高密度なノードでは決め手になります。

サイズの議論をひっくり返す共有レイヤーの計算

ここからは、サイズレポートには示せない部分です。あのレポートはイメージを 1 つずつ独立に測っているからです。

コンテナーイメージはコンテンツアドレッシングされたレイヤーの集まりです。10 個のサービスがすべて FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled でビルドされていれば、それらを動かすノードはそのランタイムレイヤーを 1 回だけ取得して保存します。11 個目のサービスの限界コストは自身のアプリケーションレイヤーだけであり、framework-dependent な ASP.NET Core サービスならそれは数メガバイトの IL です。

上の chiseled の列を使って、1 ノードに 10 サービスある場合を計算してみます。

self-contained は単一イメージでは framework-dependent に 2.4 倍の差で勝つのに、フリート規模では 3 つの中で最悪になります。トリミングはアプリケーション単位で行われ、アプリケーションをまたいで重複排除できないからです。Native AOT は十分小さいので先頭を保ちますが、その差は 4.5 倍から 2 倍を大きく下回るところまで縮みます。レジストリーのストレージ、AZ をまたぐ取得帯域、ノードのディスク圧迫は、最初の計算ではなくこの 2 番目の計算に従います。サイズを理由に何かを移行する前に、自分のフリートを測ってください。

パッチ適用: ランタイムの CVE は誰が塞ぐのか

ほとんどのチームにとって実際に判断を決めるべきなのはこの議論であり、発行の概要がはっきり書いている点でもあります。framework-dependent なアプリケーションは「アプリを実行する環境で利用可能な最新の .NET セキュリティパッチに自動的にロールフォワードする」一方、self-contained な配置は「ロールフォワードしない」ため「.NET ランタイムはアプリの新しいバージョンをリリースすることでしかアップグレードできない」とされています。

コンテナーの言葉に直すと次のようになります。

「重大な CVE を N 日以内に修正する」という統制が組織にあるなら、この差は脚注ではありません。何かに強制されない限り framework-dependent に留まるべき理由そのものです。

グローバリゼーションは chiseled と chiseled-extra を分ける隠れたスイッチ

素の -chiseled-alpine、および Azure Linux の -distroless イメージは ICU と tzdata を含まないため、グローバリゼーション不変モードのアプリケーションでしか動きません。-extra バリアントは ICU、tzdata、libstdc++ を戻すもので、サイズ表にあった 15 MB の差はこれです。

self-contained と AOT の発行では SDK が助けようとします。InvariantGlobalization が false なら -extra バリアントへ誘導してくれます。framework-dependent の発行ではファミリーを自分で選ぶので、プロパティーを合わせるのはあなたの責任です。

<!-- .NET 11, net11.0. Required if you target a plain -chiseled or -alpine base. -->
<PropertyGroup>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

ここを間違えるとコンテナーは起動時に Couldn't find a valid ICU package installed on the system で落ちます。これには専用の解決記事があります。そして不変モードはただではありません。カルチャーを考慮する文字列比較、非 ASCII に対する ToUpperToLowerTimeZoneInfo の参照はすべて挙動が変わります。何かをローカライズしたり通貨を書式化したりするなら、-extra の 15 MB を払ってください。

.NET 11 の落とし穴: ベースイメージ推論がいまだに noble を返す

コンテナーツールは推論するタグ用の Ubuntu コードネームを SDK のバージョンから計算しますが、.NET 11 のプレビュー時点でその対応表は jammy (SDK が 8.0.300 未満) と noble (8.0.300 以上) しか知りません。11.0.100 は 2 番目の条件を満たすので noble が返りますが、MCR 上の .NET 11 イメージは resolute (Ubuntu 26.04) で公開されています。結果は dotnet/sdk#53553 として報告されているとおりです。

error CONTAINER1015: Unable to access the repository 'dotnet/runtime-deps' at tag '11.0.0-preview.2-noble-chiseled-extra'

影響範囲は、まさにこの記事が扱っている経路です。framework-dependent な発行は問題ありません。コードネーム推論の分岐を通らないからです。トリミングした self-contained と PublishAot=true の発行は両方ともこれを踏みます。対処は推論に頼るのをやめてファミリーを明示することで、上のコマンドがすべてそれを渡しているのはそのためです。

# .NET 11 SDK 11.0.100-preview.7. Explicit family, no codename inference.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  -p PublishAot=true \
  -p ContainerFamily=resolute-chiseled

ContainerBaseImage に完全修飾名を設定するのも有効で、その場合は ContainerFamily を完全に迂回します。いずれにせよファミリーを明示的に固定するのは良い習慣です。将来の SDK が黙ってフリートを別のディストリビューションへ動かしてしまうのを防げます。Ubuntu 26.04 のタグのローテーションは .NET 10 側から見た同じ教訓です。

選択を決めてしまう制約

ほとんどのチームはサイズを比較検討する段階までたどり着きません。ひとつの厳しい制約が決めてしまうからです。

推奨、あらためて

既定は aspnet:11.0-<family>-chiseled 上の framework-dependent です。フリート規模で最も安いイメージであり、ランタイムの CVE がリリースではなくベースイメージの更新で済む唯一のモードであり、RID に依存しない単一成果物を出荷できる唯一のモードでもあります。コールドスタートまたはメモリー密度が拘束条件であり、依存関係ツリーがクリーンに発行できるなら runtime-deps:11.0-<family>-chiseled 上の Native AOT へ移ってください。ランタイムのバージョン固定や .NET のないベースイメージが必要な場合の中間案として self-contained + トリミングを使ってください。ただしフリート全体のストレージという観点では 3 つの中で最悪であることは理解しておいてください。どれを選ぶにせよ ContainerFamily は明示的に設定し、他の最適化に手を付ける前にイメージを chiseled にしてください。

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る