Multi Stage Builds
Wenn wir die Größe unseres Images in Docker Desktop anschauen (oder mit dem Docker-Befehl docker images oder docker image ls auf der Kommandozeile), sehen wir, dass die Größe gewaltig ist. Auf meinem Rechner ist das Image 2.6 GB groß. Warum ist es so?
docker image ls
IMAGE ID DISK USAGE CONTENT SIZEblazor-movies:from-sdk e561bfaae17f 2.55GB 758MBmcr.microsoft.com/dotnet/sdk:10.0 d1823fecac36 1.29GB 318MBUrsachen für große Images
Abschnitt betitelt „Ursachen für große Images“Schauen wir uns die Ursachen für die Größe an.
Größe des Basisimages
Abschnitt betitelt „Größe des Basisimages“Einen großen Einfluss auf die endgültige Größe hat natürlich das Basisimage. Unsere App kommt ja immer “on Top” von diesem Basisimage. Aktuell nutzen wir SDK-Image als Basis, das alleine bereits 1.3 GB groß ist. Das SDK-Image bringt alles mit, was notwendig ist, um eine dotnet Applikation ausführen, aber auch bauen zu können.
Daten im Image
Abschnitt betitelt „Daten im Image“Da wir die Applikation in dem Image selbst bauen, hat dieser nicht nur die fertige App, sondern auch noch:
- Quellcode, den wir kopiert haben, um die Applikation bauen zu können
- Eventuelle “Zwischen-Dateien”, die bei einem Build-Prozess entstehen, aber nicht in die finale App reinfließen (bei dotnet z.B. die
objundbinOrdner) - Eventuelle “Kompilate”, die auf unserem PC bereits da sind, da wir die App ja bereits local gebaut uns ausgeführt haben
Ursachen beseitigen
Abschnitt betitelt „Ursachen beseitigen“Beide Probleme können wir mit sogenannten “multi stage builds” lösen. Dabei werden unterschiedliche Images für das Erstellen der App (Kompilieren) und für finale Image genutzt. Dabei bekommt das finale Image nur die fertig kompilierte App.
Dafür müssen wir nur wenige Änderungen an unserem Bauplan durchführen.
# Image für Build FROM mcr.microsoft.com/dotnet/sdk:10.0 FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build# Verzeichnis für das BuildMit AS <name> vergeben wir diesen ersten Schritt (Stage) einen Namen, so dass wir später uns auf diesen beziehen können.
Für den zweiten Schritt benötigen wir ein schlankeres Basis-Image. Da wir am Ende die Applikation nur ausführen möchten, ist SDK überdimensioniert. Uns reicht für unsere Zwecke die runtime (Ausführung) Version. Das Image dazu bei Microsoft heißt mcr.microsoft.com/dotnet/aspnet:9.0.
Alle Schritte in der Bauanleitung (Dockerfile), die mit dem finalen Image zu tun haben, werden auf dem “runtime” Basis-Image ausgeführt.
RUN dotnet publish BlazorWebAppMovies.csproj -c Release -o /app
# Image für Runtime FROM mcr.microsoft.com/dotnet/aspnet:10.0# Verzeichnis für die AppWORKDIR /app # Kopieren des aus dem Build zu Runtime Image COPY --from=build /app .# Datenbank Connection stringENV CONNECTIONSTRINGS__BLAZORWEBAPPMOVIESCONTEXT=""- In der Zeile 4 definieren wir das Image, das als Basis für unser eigenes Image dienen soll (hier
aspnet). - In der Zeile 8 kopieren wir die fertig kompilierte Applikation aus dem SDK-Schritt (´from=build´) und Ordner
/appin den aktuellen Arbeitsordner. Damit enthält das fertige Image weder den Quellcode, noch die “Zwischen-Dateien”, die beim Kompilieren entstehen.
Bauen wir nun unser Image mit der aktuellen Version von Dockerfile und vergeben die Version from-aspnet an das Image.
# Image für BuildFROM mcr.microsoft.com/dotnet/sdk:10.0 AS build# Verzeichnis für das BuildWORKDIR /build# Kopiere das Projekt in das ArbeitsverzeichnisCOPY . ./# Wiederherstellen der AbhängigkeitenRUN dotnet restore# Veröffentlichen der App in das Verzeichnis /appRUN dotnet publish BlazorWebAppMovies.csproj -c Release -o /app
# Image für RuntimeFROM mcr.microsoft.com/dotnet/aspnet:10.0# Verzeichnis für die AppWORKDIR /app# Kopieren des aus dem Build zu Runtime ImageCOPY --from=build /app .# Datenbank Connection stringENV CONNECTIONSTRINGS__BLAZORWEBAPPMOVIESCONTEXT=""# Port für WebserverEXPOSE 8080# App in Container startenENTRYPOINT ["dotnet", "BlazorWebAppMovies.dll"]docker build -t blazor-movies:from-runtime .Unser Image ist nun deutlich kleiner geworden. Auf meinem MacBook Air ist das Image nun 462 MB, also 2.0 GB kleiner als unser erster Versuch.
docker image ls
IMAGE ID DISK USAGE CONTENT SIZEblazor-movies:from-runtime 42c8d3bed245 462MB 116MBblazor-movies:from-sdk e561bfaae17f 2.55GB 758MBmcr.microsoft.com/dotnet/aspnet:10.0 eaa79205c3ad 369MB 92.4MBmcr.microsoft.com/dotnet/sdk:10.0 d1823fecac36 1.29GB 318MBEin großer Teil kommt vom kleineren Basis-Image. Diese Änderung bringt bereits über 920 MB Ersparnis (von 1.3 GB runter auf 369 MB). Der Rest kommt durch das Weglassen des Quellcodes und der temporären Dateien, die beim Kompilieren entstehen.
Weitere Optimierungen
Abschnitt betitelt „Weitere Optimierungen“Wir können unser Image noch weiter Optimieren. Die Standard-Images von Microsoft basieren normalerweise auf Debian, zwar einer sehr schlanken Version davon, aber immer noch mit einen vollwertigen Linux als Basis. Wir kennen bereits ein minimalistisches Linux, das selbst nur 20MG groß ist, alpine Linux.
Microsoft liefert alle eigene Basis-Images auch in der Alpine-Version.
# Image für BuildFROM mcr.microsoft.com/dotnet/sdk:10.0 AS build# Verzeichnis für das BuildWORKDIR /build# Kopiere das Projekt in das ArbeitsverzeichnisCOPY . ./# Wiederherstellen der AbhängigkeitenRUN dotnet restore# Veröffentlichen der App in das Verzeichnis /appRUN dotnet publish BlazorWebAppMovies.csproj -c Release -o /app
# Image für RuntimeFROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine# Verzeichnis für die AppWORKDIR /app# Kopieren des aus dem Build zu Runtime ImageCOPY --from=build /app .# Datenbank Connection stringENV CONNECTIONSTRINGS__BLAZORWEBAPPMOVIESCONTEXT=""# Port für WebserverEXPOSE 8080# App in Container startenENTRYPOINT ["dotnet", "BlazorWebAppMovies.dll"] docker build -t blazor-moviews:from-alpine .Nun sieht unser Image noch kleiner aus. Das Basis-Image ist von 369 MB runter auf 183 MB. Und unser eigener Image ist nun von 462 MB runter auf 276 MB. Das ist 11% von unseren Ausgangspunkt.
docker image ls
IMAGE ID DISK USAGE CONTENT SIZEblazor-movies:from-alpine bb332d7cf91c 276MB 76MBblazor-movies:from-runtime 42c8d3bed245 462MB 116MBblazor-movies:from-sdk e561bfaae17f 2.55GB 758MBmcr.microsoft.com/dotnet/aspnet:10.0 eaa79205c3ad 369MB 92.4MBmcr.microsoft.com/dotnet/aspnet:10.0-alpine 1be14b20e4ec 183MB 51.9MBmcr.microsoft.com/dotnet/sdk:10.0 d1823fecac36 1.29GB 318MBAbhängig von der Programmiersprache und deren Fähigkeiten, lässt sich die finale Applikation noch weiter verkleinern (und somit auch das Image). Das erfordert aber sehr oft deutlich mehr aufwand. Mit Asp.Net wäre es möglich bei einigen Applikationen AOT (Ahead of Time) Kompilierung durchzuführen, so dass gar keine Runtime mehr benötigt wird. Mit Trimming (Abschneiden der nicht genutzten Fähigkeiten) kann die kompilierte Applikation noch weiter verkleinert werden.
Mit den einfachen Mitteln haben wir aber bereits ein sehr kleines Image erstellt, das für die meisten Anforderungen gut genug ist.
Vorteil von Alpine Linux
Abschnitt betitelt „Vorteil von Alpine Linux“Der größte Vorteil von Alpine Linux ist nicht die Größe, sondern die Sicherheit. Da dieses Linux fast nichts hat, kann es weniger kompromittiert werden. Das ist direkt sichtbar in Docker Desktop. Für SDK Image liegen aktuell 14 bekannte Schwachstellen (7 in Debian und 7 in SDK selbst), Runtime hat immer noch 8, Alpine hat nur 2 (mit niedriger Wertung).

