18 Punkte von xguru 2024-11-17 | 6 Kommentare | Auf WhatsApp teilen
  • Beim Erstellen von Docker-Container-Images ist die Wahrscheinlichkeit groß, dass unnötige Dateien enthalten sind, wenn das Dockerfile keine Multi-Stage-Struktur hat
  • Das führt zu größeren Images und mehr Sicherheitslücken
  • Es werden die Hauptursachen für „unnötige Dateien“ in Container-Images analysiert und erklärt, wie sich diese mit Multi-Stage Build beheben lassen

Ursachen für wachsende Image-Größen

  • Anwendungen haben Abhängigkeiten zur Build-Zeit und zur Laufzeit.
  • Build-Time-Abhängigkeiten sind zahlreicher als Laufzeit-Abhängigkeiten und enthalten mehr Sicherheitslücken (CVEs).
  • Wenn dasselbe Image zum Bauen und Ausführen verwendet wird, werden unnötige Build-Time-Abhängigkeiten wie Compiler oder Linter mit aufgenommen.
  • Build- und Runtime-Images sollten getrennt werden, was jedoch oft übersehen wird.

Beispiel für eine fehlerhafte Dockerfile-Struktur

Falsches Beispiel für eine Go-Anwendung

FROM golang:1.23  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
CMD ["/app/binary"]  
  • Das Image golang:1.23 ist zum Kompilieren gedacht. Wird es unverändert in der Produktion eingesetzt, enthält es auch den kompletten Go-Compiler samt Abhängigkeiten.
  • Image-Größe: über 800 MB, mit mehr als 800 Sicherheitslücken.

Falsches Beispiel für eine Node.js-Anwendung

FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
ENV NODE_ENV=production  
EXPOSE 3000  
CMD ["node", "/app/.output/index.mjs"]  
  • Der Ordner node_modules enthält dadurch auch Entwicklungsabhängigkeiten, die zur Laufzeit nicht benötigt werden.
  • Das lässt sich nicht einfach mit npm ci --omit=dev beheben, da dabei Entwicklungsabhängigkeiten entfernt werden können, die während des Build-Prozesses benötigt werden.

Methoden zur Erstellung schlanker Images vor Multi-Stage Build

Builder-Pattern

  1. Die Anwendung wird in Dockerfile.build gebaut:
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. Die gebauten Artefakte werden auf den Host kopiert:
docker cp $(docker create build:v1):/app/.output .  
  1. In Dockerfile.run wird das Runtime-Image erstellt:
FROM node:lts-slim  
WORKDIR /app  
COPY .output .  
CMD ["node", "/app/.output/index.mjs"]  
•	Nachteile: mehrere Dockerfiles, Verwaltung der Build-Reihenfolge erforderlich, zusätzliche Skripte nötig.  

Multi-Stage Build verstehen

  • Multi-Stage Build ist eine in Docker integrierte Umsetzung des Builder-Patterns.
    • Mit mehreren FROM-Anweisungen lassen sich Build- und Runtime-Stage in einem einzigen Dockerfile definieren.
    • Mit COPY --from=<stage> werden Dateien übernommen, die in einer vorherigen Stage gebaut wurden.

Beispiel für ein Multi-Stage-Dockerfile (Node.js)

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM node:lts-slim AS runtime  
WORKDIR /app  
COPY --from=build /app/.output .  
ENV NODE_ENV=production  
CMD ["node", "/app/.output/index.mjs"]  
  • Durch das direkte Kopieren der Build-Artefakte mit COPY --from=build können Dateien ohne Umweg über den Host verschoben werden.

Praxisbeispiele für Multi-Stage Build

React-Anwendung

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM nginx:alpine  
COPY --from=build /app/build /usr/share/nginx/html  
ENTRYPOINT ["nginx", "-g", "daemon off;"]  
  • Eine React-Anwendung besteht nach dem Build aus statischen Dateien und kann mit Nginx ausgeliefert werden.

Go-Anwendung

# Build stage  
FROM golang:1.23 AS build  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
  
# Runtime stage  
FROM gcr.io/distroless/static-debian12:nonroot  
COPY --from=build /app/binary /app/binary  
ENTRYPOINT ["/app/binary"]  
  • Durch die Verwendung eines distroless-Images wird eine minimale Runtime-Umgebung bereitgestellt.

Java-Anwendung

# Build stage  
FROM eclipse-temurin:21-jdk-jammy AS build  
WORKDIR /build  
COPY . .  
RUN ./mvnw package -DskipTests  
  
# Runtime stage  
FROM eclipse-temurin:21-jre-jammy  
COPY --from=build /build/target/app.jar /app.jar  
CMD ["java", "-jar", "/app.jar"]  
  • Für den Build wird das JDK verwendet, für die Laufzeit die leichtere JRE.

Fazit

  • Multi-Stage Build trennt Build- und Runtime-Umgebung und verhindert so wachsende Image-Größen durch unnötige Entwicklungsabhängigkeiten
  • Dadurch lassen sich Image-Größe reduzieren, Sicherheit verbessern und Build-Prozesse vereinfachen
  • Multi-Stage Build ist eine Standardmethode für effiziente Container-Images und unterstützt auch erweiterte Funktionen wie Verzweigungsbedingungen oder Unit-Tests während des Builds

6 Kommentare

 
savvykang 2024-11-18

Im Fall von Java wurde jlink zwar ab Version 9 eingeführt, aber die Nutzbarkeit ist nicht besonders gut, weil man abhängige Module mit jdeps finden und explizit angeben muss. Wenn man sieht, dass viele Leute solche Methoden nicht kennen oder nach einer JRE suchen, scheint es, als würde es den Java-Tools an Bekanntheit fehlen, und es wäre wohl sinnvoll, sie so zu verbessern, dass sich mit einem einzigen Befehl eine JRE erzeugen lässt.

 
brainer 2024-11-17

Ich nutze es zwar so, aber ein Nachteil scheint mir zu sein, dass die Build-Zeit lange dauert.

 
kandk 2024-11-18

Die Build-Zeit sollte keinen Unterschied machen. Wenn es einen Unterschied gibt, ist es falsch konfiguriert!

 
brainer 2024-11-18

Ah, ich verstehe!

 
qurare 2024-11-18

Je nach Strategie kann man sogar eine ganze Stage komplett cachen, sodass sich bei mir die Build-Zeit eher verkürzt hat!

 
brainer 2024-11-18

Ich sollte wohl noch etwas mehr über Docker lernen!