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:
Für detaillierte Logs kannst du --progress=plain
hinzufügen.
In einer Docker-Compose-Datei aktivierst du Buildkit wie folgt:
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:
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:
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:
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++):
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:
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:
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:
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 npm, apt, Maven oder Gradle.
Beispiel für npm:
Beispiel für Maven:
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:
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:
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:
Weitere Best Practices
- Verwende LABEL statt MAINTAINER:
Erklärung: LABEL 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. ARG: ENV 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:
Erklärung: Das Kombinieren von Befehlen reduziert die Anzahl der Layer und verringert somit die Größe des endgültigen Images.
- COPY statt ADD: ADD 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.