Docker-Container erstellen – Best Practices

Seit der Container-Revolution vor einigen Jahren hat sich Docker als Standard etabliert, obwohl Alternativen wie Podman, Buildah und Kubernetes-native Lösungen an Bedeutung gewonnen haben. Docker bleibt jedoch der De-facto-Standard in vielen Bereichen, da es eine breite Community und umfassende Unterstützung bietet.

Was ist Docker?

Docker ist eine Plattform zur Containerisierung von Anwendungen. Container sind leichtgewichtige, isolierte Umgebungen, die alle notwendigen Komponenten zum Ausführen einer Anwendung enthalten. Sie ermöglichen es, Anwendungen konsistent in verschiedenen Umgebungen zu entwickeln, zu testen und bereitzustellen. Docker bietet Entwicklern die Möglichkeit, mit minimalem Aufwand eine identische Umgebung auf verschiedenen Maschinen bereitzustellen und zu skalieren.

Buildkit als Standard nutzen

Seit Docker 23.0 ist Buildkit das Standard-Backend für Docker-Builds und sollte auch immer genutzt werden. Buildkit ist ein verbessertes Build-System für Docker, das den Erstellungsprozess von Containern beschleunigt und optimiert.

Buildkit bietet folgende Vorteile:

  • Intelligentes Caching: Es überspringt ungenutzte Build-Stufen und überträgt nur geänderte Dateien. Buildkit speichert Zwischenergebnisse, um wiederholte Arbeiten zu vermeiden.
  • Parallelisierung: Unabhängige Build-Schritte werden gleichzeitig ausgeführt, was Zeit spart.
  • Neue Features: Zum Beispiel Cache-Mounts, SSH-Forwarding und Secret-Management.
  • Benutzerdefinierte Builder-Images: Ermöglichen eine präzisere Kontrolle über den Build-Prozess.

Aktivieren von Buildkit

Setze folgende Umgebungsvariable:

export DOCKER_BUILDKIT=1

Für detaillierte Logs kannst du --progress=plain hinzufügen.

In einer Docker-Compose-Datei aktivierst du Buildkit wie folgt:

version: '3.8' services: app: build: context: . dockerfile: Dockerfile args: BUILDKIT_INLINE_CACHE: 1

Die Aktivierung über die Docker-Daemon-Konfiguration ist in den meisten Fällen nicht notwendig. Falls du Buildkit dauerhaft aktivieren willst, kannst du es in der Docker-Daemon-Konfiguration festlegen:

{ "features": { "buildkit": true } }

 

Multi-Stage-Builds konsequent einsetzen

Multi-Stage-Builds sind der Standard, um kleinere und sicherere Container zu erstellen. Sie ermöglichen es, mehrere Phasen in einem Dockerfile zu definieren, um den Build-Prozess zu optimieren und das finale Image zu verkleinern.

Beispiel für eine saubere Trennung von Build- und Laufzeitumgebung:

# Build-Phase FROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o myapp # Laufzeitumgebung FROM gcr.io/distroless/static-debian11 COPY --from=builder /app/myapp /usr/local/bin/ CMD ["myapp"]

Vorteile:

  • Die finale Laufzeitumgebung enthält nur das Nötigste.
  • Sicherheitsrisiken durch unnötige Werkzeuge oder Abhängigkeiten werden minimiert.
  • Zwischengespeicherte Stufen können benannt und wiederverwendet werden.

Beispiel für eine saubere Trennung in einer Node.js-Anwendung:

# Build-Phase FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Produktions-Phase FROM nginx:alpine COPY --from=builder /app/build /usr/share/nginx/html

Erklärung: Dieses Beispiel zeigt, wie du eine Node.js-Anwendung baust und dann nur die fertigen Dateien in einen Nginx-Server kopierst. Für Anfänger ist es hilfreich, zunächst mit einem einfachen Multi-Stage-Build zu starten, bevor komplexere Builds umgesetzt werden.

Beispiel für eine kompilierte Sprache (z.B. C++):

# Build-Phase FROM gcc:latest AS builder WORKDIR /src COPY . . RUN g++ -o myapp main.cpp # Laufzeitumgebung FROM debian:bullseye-slim COPY --from=builder /src/myapp /usr/local/bin/ CMD ["myapp"]

Erklärung: In diesem Beispiel wird ein C++-Programm in der Build-Phase mit einem GCC-Compiler erstellt. Nur das kompilierte Binary wird in die finale Laufzeitumgebung kopiert. Dies minimiert die Größe des Containers und vermeidet, dass der gesamte Build-Prozess in die Produktionsumgebung gelangt.

Container-Sicherheit priorisieren

Nicht als Root ausführen

Es ist wichtig, Container nicht als Root auszuführen, um die Auswirkungen von Sicherheitslücken zu minimieren. Wenn ein Container als Root ausgeführt wird und ein Angreifer Zugriff auf den Container erhält, kann er mit den höchsten Rechten auf das System zugreifen. Das Ausführen von Containern als nicht-privilegierter Benutzer hilft, den Schaden zu begrenzen, da der Container nur mit den Rechten des Benutzers agieren kann.

Füge am Ende des Builds einen Benutzer hinzu:

RUN addgroup --system appgroup && adduser --system --group appuser USER appuser

Minimalistische Basis-Images verwenden

Distroless- oder Alpine-Images sind kompakt und sicher. Sie enthalten nur das Nötigste, um die Anwendung auszuführen, ohne unnötige Pakete oder Tools. Beachte jedoch, dass Distroless-Images einige Nachteile mit sich bringen können, insbesondere im Hinblick auf das Debugging. Es fehlen grundlegende Tools wie Shells und Debugging-Utilities, was die Fehlersuche erschwert.

Vergleich für Anfänger:

  • Ubuntu: ~70MB
  • Alpine: ~5MB
  • Distroless: Sehr minimalistisch, aber keine Shell oder Debugging-Tools.

Wenn Debugging wichtig ist, könnte es sinnvoller sein, auf Debian Slim oder Ubuntu Minimal umzusteigen.

Sicherheits-Scanner nutzen

Tools wie Trivy oder Docker Scout helfen dir, Schwachstellen in Images zu erkennen und zu beheben. Trivy ist ein Open-Source-Tool, das auf bekannte Sicherheitslücken in Paketen, Betriebssystemen und anderen Abhängigkeiten überprüft.

Beispiel zur Verwendung von Trivy:

# Führe Trivy auf einem Image aus und beschränke die Ausgabe auf Schwachstellen mit hoher Schwere trivy image --severity HIGH myapp:latest

Container-Isolation verbessern

Seccomp (Secure Computing Mode) ist eine Sicherheitsfunktion, die den Systemaufruf-Zugriff für den Container einschränkt. Dies reduziert das Risiko, dass ein Angreifer mit kompromittierten Container-Code auf kritische Systemressourcen zugreift. In Verbindung mit read-only und tmpfs können Container weiter abgesichert werden.

Erklärung:

  • seccomp-Profile definieren, welche Systemaufrufe für den Container zulässig sind. Dadurch wird das Risiko verringert, dass ein Angreifer gefährliche Systemaufrufe ausführen kann.
  • read-only stellt sicher, dass der Container keine schreibbaren Dateisysteme hat, was zu einer weiteren Isolierung führt.
  • tmpfs erstellt temporäre Dateisysteme im Arbeitsspeicher, die beim Neustart des Containers gelöscht werden.

Beispiel:

docker run --security-opt seccomp=default.json --read-only --tmpfs /tmp myapp

Optimierung von Builds

Cache-Mounts verwenden

Cache-Mounts beschleunigen den Build-Prozess, indem sie Daten zwischen Builds speichern. Sie sind besonders nützlich bei Package-Managern wie npmaptMaven oder Gradle.

Beispiel für npm:

RUN --mount=type=cache,target=/root/.npm \ npm install

Beispiel für Maven:

RUN --mount=type=cache,target=/root/.m2 \ mvn clean install

Erklärung: Cache-Mounts speichern Abhängigkeiten oder Build-Daten zwischen verschiedenen Builds, was die Build-Zeit erheblich reduziert.

.dockerignore verwenden

Verwende .dockerignore, um unnötige Dateien vom Build-Kontext auszuschließen und die Build-Geschwindigkeit sowie Sicherheit zu verbessern. Typische Beispiele für Einträge in einer .dockerignore-Datei sind:

.git node_modules *.log target/ build/

Plattformübergreifende Builds

Die Multi-Arch-Unterstützung ist wichtig, um Container auf verschiedenen Architekturen wie ARM und AMD64 bereitzustellen. Das ist besonders relevant für die Bereitstellung von Anwendungen auf verschiedenen Geräten, von Servern bis zu mobilen Geräten oder IoT-Geräten.

Beispiel für die Erstellung eines Multi-Arch-Images:

docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest . --push

Gesundheitschecks und Robustheit

HEALTHCHECK

Ein HEALTHCHECK stellt sicher, dass der Container korrekt läuft. Die start-period-Option ist wichtig, um dem Container Zeit zu geben, vollständig zu starten, bevor die Gesundheitsprüfung beginnt. Ohne diese Option könnte der Container als fehlerhaft markiert werden, wenn er noch nicht bereit ist.

Beispiel:

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1

Weitere Best Practices

  • Verwende LABEL statt MAINTAINER:
    LABEL maintainer="Dein Name (email@example.com)"

    ErklärungLABEL ist flexibler als MAINTAINER und kann für eine Vielzahl von Metadaten verwendet werden. Es ermöglicht es, verschiedene Informationen wie Version, Autor oder Lizenz im Image zu speichern.

  • ENV vs. ARGENV wird im endgültigen Image gespeichert, während ARG nur während des Build-Prozesses verfügbar ist. Wenn du vertrauliche Daten speichern möchtest, solltest du stattdessen Secrets oder Umgebungsvariablen zur Laufzeit verwenden.
  • Kombiniere Befehle zu minimalen Layern:
    RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*

    Erklärung: Das Kombinieren von Befehlen reduziert die Anzahl der Layer und verringert somit die Größe des endgültigen Images.

  • COPY statt ADDADD kann auch Tar-Archive extrahieren und URLs herunterladen. Diese Funktionen sind jedoch selten notwendig und können zu unerwartetem Verhalten führen. In den meisten Fällen sollte COPY bevorzugt werden.

Zusätzlicher Vorschlag

  • Docker Scout: Docker Scout ist ein weiteres wertvolles Tool zur Analyse von Docker-Images. Es hilft, Schwachstellen und nicht verwendete Abhängigkeiten zu finden und ermöglicht eine detaillierte Sicherheitsanalyse.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert