Start Debugging

Framework-abhängig vs. eigenständig vs. Native AOT für ein .NET 11 Container-Image

Framework-abhängig auf einem chiseled aspnet-Image ist der richtige Standard für einen ASP.NET Core Dienst unter .NET 11, weil die Laufzeit-Schicht zwischen Diensten geteilt wird und eine Laufzeit-CVE durch einen Wechsel des Basis-Images behoben wird. Eigenständig mit Trimming und Native AOT erkaufen ein 2- bis 5-mal kleineres Image und einen deutlich schnelleren Kaltstart und kosten genau das. Echte veröffentlichte Größen, die Rechnung der geteilten Layer und der .NET 11 Basis-Image-Inferenzfehler, der den AOT-Pfad bricht.

Für einen gewöhnlichen, langlaufenden ASP.NET Core Dienst unter .NET 11 veröffentlichen Sie framework-abhängig auf einem chiseled aspnet-Image. Das ist das Kleinste, was Sie tatsächlich ausliefern (ein paar Megabyte Anwendung auf einer Laufzeit-Schicht, die Ihre anderen Dienste ohnehin schon geladen haben), und eine Laufzeit-CVE wird durch einen Rebuild auf einem neuen Basis-Image-Tag behoben statt durch Rebuild, erneutes Testen und erneutes Deployment der Anwendung. Wechseln Sie zu eigenständig plus Trimming, wenn die Anwendung einen bestimmten Laufzeit-Patch festschreiben oder auf einem Basis-Image ganz ohne .NET laufen muss. Greifen Sie nur dann zu Native AOT, wenn Kaltstart oder Speicher pro Pod die dominierende Einschränkung ist und dotnet publish über den gesamten Abhängigkeitsbaum keine AOT-Warnung meldet. Die Größenangaben, die für AOT herumgereicht werden, stimmen, aber für eine Flotte messen sie das Falsche: framework-abhängige Images teilen sich eine einzige Laufzeit-Schicht über alle Dienste eines Knotens, eigenständige und AOT-Images nicht.

Alles hier zielt auf <TargetFramework>net11.0</TargetFramework>. .NET 11 steht beim Schreiben dieses Textes bei Preview 7 (11.0.100-preview.7.26381.103, veröffentlicht am 2026-08-11), die finale Version wird für November 2026 erwartet. Preview-Image-Tags tragen einen -preview-Qualifizierer, den die finale Version fallen lässt, aus 11.0-preview-resolute-chiseled wird im November also 11.0-resolute-chiseled. Die Mechanik unten ist seit .NET 8 stabil, fast alles gilt daher unverändert für .NET 9 und .NET 10.

Die drei Modi als Container-Images

EigenschaftFramework-abhängigEigenständig + TrimmingNative AOT
Basis-Image-Repositorydotnet/aspnet oder dotnet/runtimedotnet/runtime-depsdotnet/runtime-deps
Die Laufzeit liegt inder Basis-Image-SchichtIhrer Anwendungsschichtin die Binärdatei kompiliert
Laufzeit-Schicht über Dienste geteiltJaNeinNein
Laufzeit-CVE behoben durchneues Basis-Tag ziehen, Rebuildneues SDK, Rebuild, Retest, Redeploymentneues SDK, Rebuild, Retest, Redeployment
Rollt auf installierten Patch vorJaNeinNein
Aktiviert durchnichts (ist der Standard)--self-contained -p:PublishTrimmed=true-p:PublishAot=true
Benötigt eine RIDNeinJaJa
Build-Host braucht C-ToolchainNeinNeinJa (clang, zlib1g-dev)
Reflection, Reflection.Emit, Plugin-LadenVollständigTrimming-Warnungen, Laufzeitfehler möglichEingeschränkt oder nicht verfügbar
Beispiel-Image, komprimiert52,81 MB21,86 MB11,60 MB

Die letzten drei Zahlen stammen aus dem .NET Container-Image-Größenbericht in dotnet/dotnet-docker, gemessen am Beispiel releasesapi gegen .NET 10.0 mit noble-chiseled-Basis-Images. Die vollständigen Details gleich, denn genau diese Zeile führt in die Irre.

Was jeder Modus tatsächlich ins Image legt

Das Container-Tooling des SDK leitet das Basis-Image aus Ihrem Projekt ab, und die Regel ist kurz. Laut der Containerisierungsreferenz bekommt ein eigenständiges Projekt mcr.microsoft.com/dotnet/runtime-deps, ein ASP.NET Core Projekt bekommt mcr.microsoft.com/dotnet/aspnet, und alles andere bekommt mcr.microsoft.com/dotnet/runtime. Das Tag ist der numerische Teil Ihres TFM, mit ContainerFamily als angehängtem Suffix.

Diese Ableitung ist die ganze Geschichte:

Alle drei erben dieselbe Härtung der Microsoft-Images: den Nicht-Root-Benutzer app mit UID 1654, über $APP_UID verfügbar, und Port 8080 statt 80, beides mit .NET 8 eingeführt. Chiseled-Images liefern zusätzlich keine Shell, keinen Paketmanager und kein curl mit, Debugging per docker exec und Shell-basierte Health Checks funktionieren also in keinem der drei Modi, sobald Sie eine chiseled Familie wählen.

Wie Sie jeden der drei veröffentlichen

Framework-abhängig, ohne RID, direkt auf eine chiseled ASP.NET Core Basis:

# .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

Eigenständig mit Trimming. PublishTrimmed impliziert SelfContained, aber schreiben Sie beides aus, damit ein späterer Leser sich das nicht merken muss:

# .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 impliziert eigenständig und braucht die C-Toolchain der Plattform auf der Build-Maschine:

# .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

Wenn Sie das lieber aus der CI heraus tun, ohne clang auf dem Agent zu installieren: dafür gibt es die AOT-Images des SDK:

# .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"]

Die vollständige Menge der Container*-Eigenschaften, die Tag-Steuerung und die Registry-Authentifizierung beschreibt der Durchgang zum Veröffentlichen einer .NET 11 Anwendung als Container-Image ohne Dockerfile.

Die veröffentlichten Größenzahlen

Microsoft veröffentlicht gemessene Größen für eine minimale Beispiel-Web-API über alle Basis-Image-Varianten hinweg, Spekulation ist also unnötig. Dies sind die komprimierten Größen des Beispiels releasesapi unter .NET 10.0:

Basis-ImageFramework-abhängigEigenständig + TrimmingNative AOT
Vollständiges 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

Zwei Dinge fallen sofort aus dieser Tabelle. Erstens: die Basis-Image-Familie ist ein größerer Hebel als der Deployment-Modus. Eine framework-abhängige Anwendung vom vollständigen Ubuntu-Image auf noble-chiseled zu ziehen spart 39,67 MB, mehr als dieselbe Anwendung auf dem vollständigen Image von framework-abhängig auf Native AOT umzustellen einspart (41,21 MB), und es kostet keine der Kompatibilitätsarbeit. Wenn Sie noch nicht auf chiseled sind, tun Sie das zuerst und messen Sie neu, bevor Sie irgendetwas anderes erwägen.

Zweitens: chiseled Native AOT ist tatsächlich rund 4,5-mal kleiner als chiseled framework-abhängig. Das ist ein echter Gewinn, und für eine Scale-to-Zero-Funktion oder einen Knoten mit sehr hoher Dichte ist er ausschlaggebend.

Die Layer-Rechnung, die das Größenargument umdreht

Hier ist der Teil, den der Größenbericht nicht zeigen kann, weil er ein Image isoliert misst.

Container-Images sind inhaltsadressierte Layer. Wenn zehn Ihrer Dienste alle FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled bauen, zieht und speichert jeder Knoten, der sie ausführt, diese Laufzeit-Schicht genau einmal. Die Grenzkosten des elften Dienstes sind seine eigene Anwendungsschicht, für einen framework-abhängigen ASP.NET Core Dienst also ein paar Megabyte IL.

Rechnen Sie das für zehn Dienste auf einem Knoten durch, mit der chiseled Spalte von oben:

Eigenständig ist im Flottenmaßstab der schlechteste der drei, obwohl es bei einem einzelnen Image framework-abhängig um den Faktor 2,4 schlägt, denn Trimming ist anwendungsspezifisch und kann nicht über Anwendungen hinweg deduplizieren. Native AOT ist klein genug, um vorn zu bleiben, aber sein Vorsprung schrumpft von 4,5-fach auf deutlich unter das Doppelte. Registry-Speicher, Pull-Bandbreite über Availability Zones hinweg und Plattendruck auf dem Knoten folgen dieser zweiten Rechnung, nicht der ersten. Messen Sie Ihre eigene Flotte, bevor Sie irgendetwas aus Größengründen migrieren.

Patching: wer eine Laufzeit-CVE behebt

Das ist das Argument, das für die meisten Teams tatsächlich entscheiden sollte, und es steht unmissverständlich in der Veröffentlichungsübersicht. Eine framework-abhängige Anwendung “rollt automatisch auf den neuesten in der Umgebung verfügbaren .NET-Sicherheitspatch vor”, während eine eigenständige Bereitstellung “nicht vorrollt” und “die .NET-Laufzeit nur durch die Veröffentlichung einer neuen Version der Anwendung aktualisiert werden kann”.

In Container-Begriffen:

Wenn Ihre Organisation eine Vorgabe “kritische CVEs innerhalb von N Tagen patchen” hat, ist dieser Unterschied keine Fußnote. Er ist der Grund, framework-abhängig zu bleiben, solange Sie nichts zwingt.

Globalisierung ist der versteckte Schalter zwischen chiseled und chiseled-extra

Einfache -chiseled-, -alpine- und Azure Linux -distroless-Images kommen ohne ICU und tzdata, sie funktionieren also nur für Anwendungen im Globalization Invariant Mode. Die -extra-Varianten bringen ICU, tzdata und libstdc++ zurück, und genau daher kommen die 15 MB Differenz in der Größentabelle.

Bei eigenständigen und AOT-Veröffentlichungen versucht das SDK zu helfen: ist InvariantGlobalization false, lenkt es Sie auf eine -extra-Variante. Bei framework-abhängigen Veröffentlichungen wählen Sie die Familie selbst, es liegt also an Ihnen, die Eigenschaft passend zu setzen:

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

Machen Sie das falsch, stirbt der Container beim Start mit Couldn't find a valid ICU package installed on the system, wofür es einen eigenen Fix-Artikel gibt. Und der Invariant Mode ist nicht kostenlos: kultursensitiver Zeichenkettenvergleich, ToUpper und ToLower für Nicht-ASCII sowie TimeZoneInfo-Abfragen ändern ihr Verhalten. Wenn Sie irgendetwas lokalisieren oder Währungen formatieren, zahlen Sie die 15 MB für -extra.

Der .NET 11 Stolperstein: die Basis-Image-Ableitung sagt weiterhin noble

Das Container-Tooling berechnet den Ubuntu-Codenamen für das abgeleitete Tag aus der SDK-Version, und in den .NET 11 Previews kennt diese Zuordnung nur jammy (SDK unter 8.0.300) und noble (8.0.300 und höher). Da 11.0.100 die zweite Bedingung erfüllt, liefert sie noble, .NET 11 Images auf MCR erscheinen aber unter resolute (Ubuntu 26.04). Das Ergebnis, gemeldet als dotnet/sdk#53553:

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

Der Schadensradius sind genau die Pfade, um die es in diesem Artikel geht. Framework-abhängiges Veröffentlichen ist unbetroffen, weil es den Codename-Ableitungszweig nicht durchläuft. Getrimmt eigenständige und PublishAot=true-Veröffentlichungen laufen beide hinein. Die Lösung ist, sich nicht auf die Ableitung zu verlassen und die Familie explizit zu benennen, weshalb alle Kommandos oben sie mitgeben:

# .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 auf einen vollqualifizierten Namen zu setzen funktioniert ebenfalls und umgeht ContainerFamily vollständig. Die Familie explizit festzuschreiben ist ohnehin gute Praxis: sie hindert ein künftiges SDK daran, Ihre Flotte still auf eine andere Distribution zu verschieben. Die Tag-Rotation von Ubuntu 26.04 ist dieselbe Lektion von der .NET 10 Seite.

Die Einschränkung, die für Sie entscheidet

Die meisten Teams kommen nie zum Abwägen der Größen, weil eine harte Einschränkung entscheidet:

Empfehlung, noch einmal

Standard ist framework-abhängig auf aspnet:11.0-<family>-chiseled. Es ist das billigste Image im Flottenmaßstab, es ist der einzige Modus, in dem eine Laufzeit-CVE ein Basis-Image-Update statt eines Releases ist, und es ist der einzige, der ein einziges RID-unabhängiges Artefakt liefert. Wechseln Sie zu Native AOT auf runtime-deps:11.0-<family>-chiseled, wenn Kaltstart oder Speicherdichte die bindende Einschränkung ist und Ihr Abhängigkeitsbaum sauber veröffentlicht. Nehmen Sie eigenständig plus Trimming als Mittelweg, wenn Sie die Laufzeitversion festschreiben oder ein Basis-Image ohne .NET brauchen, im Bewusstsein, dass es für flottenweiten Speicher der schlechteste der drei ist. Was immer Sie wählen: setzen Sie ContainerFamily explizit, und stellen Sie das Image auf chiseled um, bevor Sie irgendetwas anderes optimieren.

Verwandt

Quellen

Comments

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

< Zurück