Einleitung

Das Dockerfile entscheidet über den Großteil von Größe, Build-Geschwindigkeit und Angriffsfläche Ihres Images, bevor auch nur ein Container startet. Die folgenden Praktiken wenden wir in jedem Projekt an — Performance und Sicherheit gemeinsam, denn in einem Dockerfile sind sie ein und dieselbe Disziplin. Hier ein Referenz-Multi-Stage-Build für einen Node-22-Service, auf den sich die meisten Abschnitte unten zurückbeziehen:

# syntax=docker/dockerfile:1

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:22-bookworm-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD ["node", "dist/healthcheck.js"]
CMD ["node", "dist/server.js"]

Build-Praktiken

1. Mit einem minimalen, unterstützten Base-Image starten — und es pinnen

Verwenden Sie Slim-, Alpine- oder Distroless-Varianten offizieller Images und bauen Sie niemals auf einem OS-Release auf, das sein Lebensende erreicht hat (Ubuntu 20.04 etwa fiel im April 2025 aus dem Standard-Support). Ein Tag wie 22-bookworm-slim ist veränderlich; pinnen Sie für reproduzierbare Builds per Digest und lassen Sie Renovate oder Dependabot Updates vorschlagen:

docker buildx imagetools inspect node:22-bookworm-slim
# dann pinnen:  FROM node:22-bookworm-slim@sha256:<digest>
2. Multi-Stage-Builds verwenden

Compiler, Dev-Dependencies und Build-Caches gehören in eine Build-Stage, die anschließend verworfen wird. Die Runtime-Stage oben liefert nur das gebaute Ergebnis und die Produktionsabhängigkeiten aus — kleineres Image, schnellere Pulls und deutlich weniger Pakete, die ein CVE-Scanner anschlagen lassen. Bei kompilierten Sprachen kann die Runtime-Stage ein Distroless-Image ganz ohne Shell sein.

3. Instruktionen für das Layer-Caching ordnen

Docker cacht Layer von oben nach unten und invalidiert alles unterhalb der ersten Änderung. Kopieren Sie Dependency-Manifeste und installieren Sie Pakete, bevor Sie den Quellcode kopieren — wie im Beispiel — damit eine gewöhnliche Codeänderung den gecachten npm ci-Layer wiederverwendet, statt alles neu zu installieren.

4. Im selben RUN-Layer aufräumen

Jedes RUN erzeugt einen Layer; Dateien, die in einer späteren Instruktion gelöscht werden, stecken weiterhin im früheren Layer. Verketten Sie Installation und Aufräumen und pinnen Sie Versionen, damit der Build tatsächlich reproduzierbar ist:

RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl \
 && rm -rf /var/lib/apt/lists/*
5. .dockerignore nutzen und COPY statt ADD bevorzugen

Eine .dockerignore, die .git, node_modules, .env und lokale Build-Artefakte ausschließt, hält den Build-Kontext klein und hält Secrets und Ballast aus COPY . . heraus. Verwenden Sie COPY, sofern Sie nicht gezielt die Archiv-Extraktion von ADD benötigen — das URL-Fetching von ADD ist eine klassische Quelle für Überraschungsinhalte in Images.

Sicherheitspraktiken

6. Als Non-Root-User laufen lassen

Ein Container, der als Root läuft, ist nur einen Kernel-Bug davon entfernt, Root auf dem Host zu sein. Offizielle Sprach-Images bringen einen unprivilegierten User mit (node oben); andernfalls legen Sie mit useradd einen an und wechseln vor dem CMD per USER. Installieren Sie nur, was die Anwendung braucht — jedes zusätzliche Paket ist Angriffsfläche und Scanner-Rauschen.

7. Secrets niemals in Layer einbacken

Tokens, die über ARG, ENV oder eine kopierte und später gelöschte Datei hereinkommen, bleiben aus der Image-History rekonstruierbar. BuildKit-Secret-Mounts stellen Credentials nur der Instruktion zur Verfügung, die sie braucht, und hinterlassen in keinem Layer etwas:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

# zur Build-Zeit:
docker build --secret id=npmrc,src=$HOME/.npmrc -t shop-api .
8. Bei jedem Build linten und scannen

Linten Sie Dockerfiles mit hadolint und scannen Sie gebaute Images in der CI mit Trivy oder Grype auf CVEs — bei kritischen Findings soll die Pipeline fehlschlagen:

hadolint Dockerfile
trivy image --severity HIGH,CRITICAL --exit-code 1 shop-api:1.8.3

Eine Richtigstellung, die man explizit machen sollte, weil sie weit kursiert: Docker Bench for Security auditiert die Konfiguration von Docker-Host und -Daemon gegen den CIS-Benchmark — es scannt weder Dockerfiles noch Images. Nutzen Sie es für das Host-Hardening; für Images nehmen Sie Trivy, Grype oder Docker Scout. Scanning gehört außerdem auf die Registry-Seite, denn neue CVEs treffen auch Images, die Sie vor Wochen gebaut haben — unser Leitfaden Wo Docker-Images speichern? behandelt Registry-Scanning, Signieren und Retention.

9. Einen HEALTHCHECK ergänzen

Ein HEALTHCHECK erlaubt Docker, Compose und Orchestrierern, zwischen „Prozess läuft“ und „Service antwortet tatsächlich“ zu unterscheiden — genau darauf stützen sich Restart-Policies und depends_on-Bedingungen. Verwenden Sie die Exec-Form wie im Referenz-Dockerfile, damit der Check auch in Images ohne Shell funktioniert.

Fazit

Keine dieser Praktiken ist exotisch; die Schwierigkeit liegt darin, sie über Dutzende Repositories hinweg konsistent anzuwenden. Das ist ebenso ein Pipeline- und Policy-Problem wie ein Dockerfile-Problem — die Art Baseline, die unser SecOps-Team in die CI einbaut, damit sie durchgesetzt bleibt, und die unsere DevOps-Practice schnell hält. Starten Sie mit dem Referenz-Build oben, ergänzen Sie das Scanner-Gate und iterieren Sie von dort.

Das in Produktion betreiben? Unser AWS-Managed-Services-Team betreibt solche Umgebungen rund um die Uhr — Monitoring, Incident Response, Patching und Kostenkontrolle durch namentlich benannte Senior Engineers, ab 3.000 €/Monat.