Am Ende von Lektion 2.9.6 stand eine unbequeme Zahl: Ihr Image war mehr als fünfmal so groß wie das aus dotnet publish. Darin lagen das ganze SDK, alle NuGet-Pakete, der Quellcode und Werkzeuge wie git, curl und PowerShell. Die laufende Kladde braucht davon nichts. In dieser Lektion trennen Sie Bauen und Ausführen. Das Ergebnis ist ein Image, das kleiner ist als das aus dotnet publish, keine Shell enthält und trotzdem Deutsch und die Zeitzone des Büros kann.

Zwei Stufen in einem Dockerfile

Ein Dockerfile darf mehrere FROM enthalten. Jedes beginnt eine neue Stufe (englisch stage) mit eigenem Grund-Image. Eine spätere Stufe kann mit COPY --from= Dateien aus einer früheren holen. Ins fertige Image kommt nur die letzte Stufe; alle anderen sind Werkbänke, die nach dem Bauen weggeräumt werden.

Für die Kladde sind es zwei Stufen. Ersetzen Sie im Branch feature/dockerfile-mehrstufig das Dockerfile aus Lektion 2.9.6:

dockerfile
# Stage 1: build with the full SDK.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY global.json Directory.Build.props ./
COPY src/Kladde.Core/Kladde.Core.csproj src/Kladde.Core/
COPY src/Kladde.Data/Kladde.Data.csproj src/Kladde.Data/
COPY src/Kladde.Ted/Kladde.Ted.csproj src/Kladde.Ted/
COPY src/Kladde.ServiceDefaults/Kladde.ServiceDefaults.csproj src/Kladde.ServiceDefaults/
COPY src/Kladde.Web/Kladde.Web.csproj src/Kladde.Web/
RUN dotnet restore src/Kladde.Web/Kladde.Web.csproj
COPY src/ src/
RUN dotnet publish src/Kladde.Web/Kladde.Web.csproj -c Release --no-restore -o /app

# Stage 2: run on the chiseled ASP.NET Core image with ICU and time zones.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra AS final
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "Kladde.Web.dll"]
  • AS build gibt der ersten Stufe einen Namen. Sie ist bis zum publish dieselbe wie in Lektion 2.9.6, mit derselben Reihenfolge für den Cache.
  • Das zweite FROM beginnt von vorn, auf einem Image, das nur die ASP.NET-Core-Laufzeit enthält und kein SDK.
  • COPY --from=build /app . holt genau einen Ordner aus der ersten Stufe: die veröffentlichte Kladde. Pakete, Quellcode und Compiler bleiben drüben.
  • USER $APP_UID: Die .NET-Images setzen die Umgebungsvariable APP_UID auf 1654, die Nummer des Benutzers app. Eine Nummer statt eines Namens hat einen Vorteil: Plattformen wie Kubernetes können an ihr prüfen, dass der Container nicht als root läuft, ohne ins Image zu sehen. Die Chiseled-Images laufen ohnehin schon als app; die Zeile macht es sichtbar und gilt auch, wenn jemand das Grund-Image wechselt.
  • EXPOSE 8080 ist die Notiz aus Lektion 2.9.6 für Menschen und Werkzeuge. Den Port selbst setzt das Grund-Image mit ASPNETCORE_HTTP_PORTS=8080. Port 8080 statt 80 hat einen Grund: Ports unter 1024 darf unter Linux nur root öffnen.

Bauen Sie wie gewohnt aus dem Hauptordner der Kladde:

PowerShell
docker build -f src/Kladde.Web/Dockerfile -t kladde-web:mehrstufig --progress=plain .
docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" kladde-web
REPOSITORY   TAG          SIZE
kladde-web   mehrstufig   245MB
kladde-web   dockerfile   1.72GB
kladde-web   0.9.0        314MB

Von 1,72 Gigabyte auf 245 Megabyte, und auch kleiner als das Image aus dotnet publish, das auf dem vollen Ubuntu-Image aspnet:10.0 aufbaut. Die Zahlen stammen aus einer Beispiel-Kladde; bei Ihnen weichen sie ab.

Wollen Sie nur die erste Stufe bauen, etwa um in ihr nach einem Fehler zu suchen, sagen Sie es mit --target build. Davon macht Übung 2 Gebrauch.