Container-Image-Größe mit Docker Multi-Stage Build reduzieren
(labs.iximiuz.com)- 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.23ist 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_modulesenthält dadurch auch Entwicklungsabhängigkeiten, die zur Laufzeit nicht benötigt werden. - Das lässt sich nicht einfach mit
npm ci --omit=devbeheben, 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
- Die Anwendung wird in
Dockerfile.buildgebaut:
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
- Die gebauten Artefakte werden auf den Host kopiert:
docker cp $(docker create build:v1):/app/.output .
- In
Dockerfile.runwird 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.
- Mit mehreren
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=buildkö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
Im Fall von Java wurde
jlinkzwar ab Version 9 eingeführt, aber die Nutzbarkeit ist nicht besonders gut, weil man abhängige Module mitjdepsfinden 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.Ich nutze es zwar so, aber ein Nachteil scheint mir zu sein, dass die Build-Zeit lange dauert.
Die Build-Zeit sollte keinen Unterschied machen. Wenn es einen Unterschied gibt, ist es falsch konfiguriert!
Ah, ich verstehe!
Je nach Strategie kann man sogar eine ganze Stage komplett cachen, sodass sich bei mir die Build-Zeit eher verkürzt hat!
Ich sollte wohl noch etwas mehr über Docker lernen!