Ein Dockerfile ist kein Installationsskript. Der Unterschied entscheidet über Abbildgröße, Sicherheit und Wiederholbarkeit. Ein Installationsskript sammelt im Lauf der Zeit Rückstände und setzt auf den Zustand des Systems; ein gutes Dockerfile ist eine vollständige, deklarative Beschreibung.
Die Schichtenlogik verstehen
# Jede Anweisung erzeugt eine Schicht. Schichten sind
# unveraenderlich - sie werden nicht "zusammengefasst".
# Was einmal in einer Schicht steht, bleibt im Abbild.
FROM debian:13-slim
RUN apt-get update
RUN apt-get install -y curl # FALSCH: Update in anderer Schicht
# -> "apt-get update" und "apt-get install" in EINER Zeile!
# Sonst kann ein alter Zwischenspeicher installiert werden.
# Richtig:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# --no-install-recommends: keine unnoetigen Pakete
# rm -rf der Listen: der Zwischenspeicher wuerde sonst im
# Abbild bleiben (oft mehrere 10 MB!)
# Der Loesch-Befehl muss in DERSELBEN Schicht laufen:
RUN curl -fsSL https://example.org/gross.tar.gz -o /tmp/g.tar.gz \
&& tar -xzf /tmp/g.tar.gz -C /opt \
&& rm /tmp/g.tar.gz # geloescht in derselben Schicht
# In einer spaeteren RUN-Zeile geloescht, bleibt die Datei
# trotzdem im Abbild (in der unteren Schicht).
Ein gutes Dockerfile
# ---------- Stufe 1: bauen ----------
FROM golang:1.24-bookworm AS build
WORKDIR /src
# Abhaengigkeiten ZUERST - so wird diese Schicht zwischengespeichert,
# solange sich die Abhaengigkeitsdateien nicht aendern
COPY go.mod go.sum ./
RUN go mod download
# Erst dann der Code
COPY . .
# Umgebung: Statisch gelinkt, keine Symboltabellen
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w -X main.version=${VERSION}" \
-o /out/app ./cmd/app
# -trimpath: keine lokalen Pfade im Binaerfile
# -s -w: kleinere Datei
# WICHTIG: ein statisch gelinktes Binaerfile braucht die
# Bauumgebung zur Laufzeit nicht.
# ---------- Stufe 2: laufen ----------
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build --chown=65532:65532 /out/app /app
USER 65532:65532 # nicht root!
EXPOSE 8080
# Zustandspruefung im Abbild (wird von der Umgebung genutzt)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app", "-health"]
ENTRYPOINT ["/app"]
# ACHTUNG: ENTRYPOINT in Ausfuehrungsform (JSON-Liste),
# nicht als Zeichenkette! Sonst laeuft es ueber "sh -c"
# und die Signalbehandlung (SIGTERM) funktioniert nicht.
Die wichtigsten Regeln
# 1) Kleine Basisabbilder
# debian:13-slim ~80 MB
# alpine ~7 MB (musl statt glibc - bei
# manchen Binaerfiles problematisch)
# distroless ~2 MB (kein Shell, keine Werkzeuge -
# kleinste Angriffsflaeche)
# ABER: "Alpine ist immer besser" stimmt nicht. Ein
# statisches Go-Binaerfile ist mit distroless kleiner und
# sicherer; ein Python-Projekt hat mit slim weniger Ueberraschung.
# 2) Nicht als root laufen
USER 1000:1000
# Ohne USER laeuft PID 1 als root. Ein Ausbruch ist dann
# gleichbedeutend mit dem Administrator.
# 3) .dockerignore anlegen - WICHTIG fuer Bauzeit und Sicherheit
# .git
# node_modules
# *.log
# .env
# .venv
# __pycache__
# tests/
# Ohne .dockerignore wandert alles ins Abbild - inklusive
# .env mit Zugangsdaten! Der Kontext-Upload wird zudem
# oft um ein Vielfaches langsamer.
# 4) Genau kopieren
COPY --chown=app:app src/ /app/src/
# COPY . . ist bequem, kopiert aber Unerwartetes mit
# (Tests, Zugangsdaten, Zwischenstaende).
# 5) Zeitgeber und Signale
# Ein ENTRYPOINT ohne shell-Form ist Pflicht, damit
# SIGTERM beim Anhalten ankommt. Sonst wird nach 10 s
# hart abgebrochen (Datenverlust bei Datenbanken!).
# Alternativ: dumb-init oder tini als PID 1.
# 6) Feste Kennzeichnungen
FROM golang:1.24.6-bookworm
# NIE :latest. Sonst bauen identische Quellen zu
# unterschiedlichen Zeiten unterschiedliche Abbilder.
# Besser: Digest verwenden (FROM image@sha256:...).
# 7) Feste Zeitstempel und Reihenfolge
# Fuer wiederholbare Ergebnisse (reproduzierbare Bauvorgaenge)
# sollten Datei-Zeitstempel und Reihenfolge festgelegt sein.
# Ohne das unterscheidet sich der Digest bei jedem Bau.
# 8) Anzeigename und Urheber
LABEL org.opencontainers.image.source="https://git.example.org/team/repo"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.created="2026-09-26T00:00:00Z"
# Wichtig fuer Sicherheitspruefungen und Rueckverfolgbarkeit.
# 9) Umgang mit Geheimnissen
RUN --mount=type=secret,id=token \
TOKEN=$(cat /run/secrets/token) ./skript.sh
# NIEMALS ARG oder ENV fuer Geheimnisse verwenden!
# "docker history" zeigt ARG-Werte im Klartext, fuer immer.
# Secrets werden dagegen nicht in Schichten gespeichert.
# 10) Zusammenfassen, aber sinnvoll
RUN apt-get update \
&& apt-get install -y --no-install-recommends aachen b \
&& rm -rf /var/lib/apt/lists/*
# Eine Zeile pro logischer Aufgabe. Zu lange Zeilen sind
# ebenfalls schlecht - Lesbarkeit und Zwischenspeicher nuetzen.
# Zwischenspeicher: Solange sich die Zeile nicht aendert,
# wird sie nicht neu gebaut.
Prüfen und härten
# Bauen und Groesse ansehen
docker build -t app:1.0.0 .
docker images app
# Die Schichten einzeln auf Groesse pruefen
docker history --no-trunc app:1.0.0
dive app:1.0.0 # Werkzeug fuer Schichtanalyse
# Mit der Pruefung von Sicherheitsluecken
trivy image app:1.0.0
docker scout cves app:1.0.0
# Laeuft als root?
docker inspect app:1.0.0 | grep -i '"User"'
docker run --rm app:1.0.0 id # nicht: uid=0(root)
# Laufzeit absichern (mehr als nur USER!)
docker run --rm \
--read-only \ # Dateisystem nur lesen
--tmpfs /tmp \ # nur /tmp beschreibbar
--cap-drop=ALL \ # alle Faehigkeiten entfernen
--cap-add=NET_BIND_SERVICE \ # nur was wirklich noetig ist
--security-opt no-new-privileges \ # keine Rechteerhoehung
--security-opt seccomp=profil.json \
--memory 512m --cpus 1.0 \
--pids-limit 100 \
-u 1000:1000 \
app:1.0.0
# Prozessbaum im Container pruefen (PID 1 richtig?)
docker run --rm --entrypoint ps app:1.0.0 aux
# Unter distroless nicht moeglich (kein ps) - das ist gewollt.
# Fuer Kubernetes: Sicherheitskontext setzen
# securityContext:
# runAsNonRoot: true
# runAsUser: 1000
# readOnlyRootFilesystem: true
# allowPrivilegeEscalation: false
# capabilities: {drop: [ALL]}
Prüfliste
- Sind
apt-get updateundinstallin derselben Zeile? - Sind die Zwischenspeicher der Paketverwaltung gelöscht (in derselben Schicht)?
- Wird ein Mehrstufenbau verwendet (Werkzeuge nur in der Baustufe)?
- Ist
USERgesetzt (nicht root)? - Ist eine
.dockerignorevorhanden – und enthält sie.env,.gitund Zugangsdaten? - Sind Abbilder festgelegt (kein
:latest)? - Ist
ENTRYPOINTin Ausführungsform (JSON-Liste)? - Sind Geheimnisse über
--mount=type=secreteingebunden, nicht überARG/ENV? - Wird der Bauvorgang in der CI bei jeder Änderung ausgeführt und auf Sicherheitslücken geprüft?
- Sind die Laufzeitbeschränkungen gesetzt (Vorgabeparameter von Docker sind nicht sicher)?
Häufige Fehler
- Geheimnisse in
ARGoderENV. Sie stehen für immer indocker history. Auch wenn das Abbild neu gebaut wird: Die alte Schicht bleibt. - Ohne
.dockerignorebauen. Zugangsdateien landen im Abbild – und der Bau dauert ein Vielfaches. COPY . .. Tests, Zwischenstände und lokale Konfiguration wandern mit.- Installationsschritte in vielen Zeilen. Zwischenspeicher und Rückstände bleiben erhalten.
- Kein
USER. Der Klassiker. ENTRYPOINTals Zeichenkette.ENTRYPOINT "app"läuft alssh -c "app"– die Anwendung ist Kind eines Shell-Prozesses und bekommt SIGTERM nicht direkt. Beim Anhalten gehen Aufträge verloren.:latestverwenden. Der Bau ist nicht mehr wiederholbar.- Bauvorgang ohne Teststufe. Fehler werden bis in die Laufzeit verschleppt.
- Alle Fähigkeiten behalten. Ein Webserver braucht keine Fähigkeit des Kernels (
CAP_SYS_ADMIN) – aber der Standard lässt sie zu.
Verwandte Grundlagen: Dockerfile, Container-Sicherheit, Container, Docker-Befehle.