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

  1. Sind apt-get update und install in derselben Zeile?
  2. Sind die Zwischenspeicher der Paketverwaltung gelöscht (in derselben Schicht)?
  3. Wird ein Mehrstufenbau verwendet (Werkzeuge nur in der Baustufe)?
  4. Ist USER gesetzt (nicht root)?
  5. Ist eine .dockerignore vorhanden – und enthält sie .env, .git und Zugangsdaten?
  6. Sind Abbilder festgelegt (kein :latest)?
  7. Ist ENTRYPOINT in Ausführungsform (JSON-Liste)?
  8. Sind Geheimnisse über --mount=type=secret eingebunden, nicht über ARG/ENV?
  9. Wird der Bauvorgang in der CI bei jeder Änderung ausgeführt und auf Sicherheitslücken geprüft?
  10. Sind die Laufzeitbeschränkungen gesetzt (Vorgabeparameter von Docker sind nicht sicher)?

Häufige Fehler

  1. Geheimnisse in ARG oder ENV. Sie stehen für immer in docker history. Auch wenn das Abbild neu gebaut wird: Die alte Schicht bleibt.
  2. Ohne .dockerignore bauen. Zugangsdateien landen im Abbild – und der Bau dauert ein Vielfaches.
  3. COPY . .. Tests, Zwischenstände und lokale Konfiguration wandern mit.
  4. Installationsschritte in vielen Zeilen. Zwischenspeicher und Rückstände bleiben erhalten.
  5. Kein USER. Der Klassiker.
  6. ENTRYPOINT als Zeichenkette. ENTRYPOINT "app" läuft als sh -c "app" – die Anwendung ist Kind eines Shell-Prozesses und bekommt SIGTERM nicht direkt. Beim Anhalten gehen Aufträge verloren.
  7. :latest verwenden. Der Bau ist nicht mehr wiederholbar.
  8. Bauvorgang ohne Teststufe. Fehler werden bis in die Laufzeit verschleppt.
  9. 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.