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 SIZE
blazor-movies:from-sdk e561bfaae17f 2.55GB 758MB
mcr.microsoft.com/dotnet/sdk:10.0 d1823fecac36 1.29GB 318MB
Ursachen für große Images
Schauen wir uns die Ursachen für die Größe an.
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
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
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 Build
Mit 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.
1RUN dotnet publish BlazorWebAppMovies.csproj -c Release -o /app
2+
3+ # Image für Runtime
4+ FROM mcr.microsoft.com/dotnet/aspnet:10.0
5# Verzeichnis für die App
6WORKDIR /app
7+ # Kopieren des aus dem Build zu Runtime Image
8+ COPY --from=build /app .
9# Datenbank Connection string
10ENV 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 Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Verzeichnis für das Build
WORKDIR /build
# Kopiere das Projekt in das Arbeitsverzeichnis
COPY . ./
# Wiederherstellen der Abhängigkeiten
RUN dotnet restore
# Veröffentlichen der App in das Verzeichnis /app
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 App
WORKDIR /app
# Kopieren des aus dem Build zu Runtime Image
COPY --from=build /app .
# Datenbank Connection string
ENV CONNECTIONSTRINGS__BLAZORWEBAPPMOVIESCONTEXT=""
# Port für Webserver
EXPOSE 8080
# App in Container starten
ENTRYPOINT ["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 SIZE
blazor-movies:from-runtime 42c8d3bed245 462MB 116MB
blazor-movies:from-sdk e561bfaae17f 2.55GB 758MB
mcr.microsoft.com/dotnet/aspnet:10.0 eaa79205c3ad 369MB 92.4MB
mcr.microsoft.com/dotnet/sdk:10.0 d1823fecac36 1.29GB 318MB
Ein 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
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 Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Verzeichnis für das Build
WORKDIR /build
# Kopiere das Projekt in das Arbeitsverzeichnis
COPY . ./
# Wiederherstellen der Abhängigkeiten
RUN dotnet restore
# Veröffentlichen der App in das Verzeichnis /app
RUN dotnet publish BlazorWebAppMovies.csproj -c Release -o /app
# Image für Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
# Verzeichnis für die App
WORKDIR /app
# Kopieren des aus dem Build zu Runtime Image
COPY --from=build /app .
# Datenbank Connection string
ENV CONNECTIONSTRINGS__BLAZORWEBAPPMOVIESCONTEXT=""
# Port für Webserver
EXPOSE 8080
# App in Container starten
ENTRYPOINT ["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 SIZE
blazor-movies:from-alpine bb332d7cf91c 276MB 76MB
blazor-movies:from-runtime 42c8d3bed245 462MB 116MB
blazor-movies:from-sdk e561bfaae17f 2.55GB 758MB
mcr.microsoft.com/dotnet/aspnet:10.0 eaa79205c3ad 369MB 92.4MB
mcr.microsoft.com/dotnet/aspnet:10.0-alpine 1be14b20e4ec 183MB 51.9MB
mcr.microsoft.com/dotnet/sdk:10.0 d1823fecac36 1.29GB 318MB
Abhä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
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).
Das Bild der Schwachstellen ist eine Momentaufnahme und wird bei Ihnen anders aussehen. Allgemeines Bild, dass Alpine-Images deutlich weniger Schwachstellen aufweisen, wird aber bleiben.

14 Schwachstellen in SDK Image
