Containerisierung
Eine Anwendung wird zusammen mit allem, was sie zum Laufen braucht, in ein abgeschlossenes Paket gepackt. Dieses Paket läuft auf jedem kompatiblen Rechner identisch. In der Softwareentwicklung heißt dieses Verfahren Containerisierung.
Was Containerisierung leistet
Software besteht aus Code, Bibliotheken, Konfigurationsdateien und Laufzeitumgebungen. Auf dem Rechner der Entwicklerin funktioniert alles. Auf dem Produktivserver fehlt eine Bibliothek, eine Pfadangabe stimmt nicht, eine Systemversion weicht ab. Das Ergebnis: Die Anwendung startet nicht oder verhält sich anders als erwartet.
Containerisierung beseitigt diese Diskrepanz. Die Anwendung wird zusammen mit sämtlichen Abhängigkeiten in eine standardisierte Einheit verpackt. Diese Einheit, der Container, enthält alles außer dem Betriebssystem-Kern selbst. Der Kern wird vom Host-System bereitgestellt und gemeinsam genutzt.
Beispiel: Eine Python-Webanwendung benötigt Python 3.11, Flask, PostgreSQL-Treiber und bestimmte Systembibliotheken. Der Container enthält all diese Komponenten in exakt den festgelegten Versionen. Ob er auf einem Entwicklungslaptop, einem Testserver oder in einer Produktivumgebung startet, spielt keine Rolle.
Beispiel: Ein Datenverarbeitungsdienst läuft in der Entwicklung unter Ubuntu 22.04, der Produktivserver nutzt Debian 12. Ohne Container müssten Paketabhängigkeiten für beide Systeme separat gepflegt werden. Mit einem Container entfällt diese Arbeit.
Vom Dockerfile zum laufenden Container
Der technische Ablauf folgt drei Stufen. Zuerst wird eine Beschreibungsdatei erstellt, meistens ein sogenanntes Dockerfile. Diese Datei legt fest, welches Basis-Image verwendet wird, welche Pakete installiert werden und wie die Anwendung gestartet wird. Ein Build-Werkzeug erzeugt daraus ein Image: ein unveränderliches Abbild, das alle Schichten enthält. Aus diesem Image wird ein Container gestartet, ein isolierter Prozess mit eigenem Dateisystem, Netzwerk und Prozessbaum.
Beispiel: Ein Dockerfile für eine Node.js-Anwendung beginnt mit FROM node:20-alpine, kopiert den Quellcode, führt npm install aus und definiert CMD ["node", "server.js"]. Das resultierende Image ist etwa 150 MB groß und startet in unter einer Sekunde.
Images sind schichtbasiert aufgebaut. Jede Anweisung im Dockerfile erzeugt eine Schicht. Schichten, die sich nicht ändern, werden aus einem lokalen Cache geladen. Das beschleunigt wiederholte Builds erheblich.
Beispiel: Ändert sich nur der Anwendungscode, müssen Betriebssystem-Schicht und Abhängigkeitsschicht nicht neu gebaut werden. Ein inkrementeller Build dauert dann Sekunden statt Minuten.
Wie Container Isolation herstellen
Container nutzen Mechanismen des Linux-Kernels, um Prozesse voneinander abzuschotten. Die beiden zentralen Bausteine sind Namespaces und Control Groups (cgroups).
Namespaces trennen die Sicht eines Prozesses auf das System. Jeder Container erhält einen eigenen Prozessbaum (PID-Namespace), ein eigenes Netzwerk (Network-Namespace), ein eigenes Dateisystem (Mount-Namespace) und eigene Benutzer-IDs (User-Namespace). Aus Sicht des Containers existieren keine anderen Prozesse.
Control Groups begrenzen den Ressourcenverbrauch. Sie legen fest, wie viel CPU-Zeit, Arbeitsspeicher und I/O-Bandbreite ein Container maximal nutzen darf. Ein Container kann dadurch nicht das gesamte Host-System blockieren.
Beispiel: Ein Container erhält ein Speicherlimit von 512 MB. Versucht die Anwendung mehr Speicher zu allokieren, beendet der Kernel den Prozess. Andere Container und der Host bleiben davon unberührt.
Beispiel: In einem Shared-Hosting-Szenario laufen 20 Container auf einem Server. Jeder erhält maximal 5 % CPU-Zeit. Ein fehlerhafter Container mit einer Endlosschleife kann die übrigen 19 nicht ausbremsen.
Fachliche Einordnung: Container-Isolation ist schwächer als die Isolation einer virtuellen Maschine. Alle Container teilen sich denselben Kernel. Eine Kernel-Schwachstelle kann die Grenze zwischen Containern aufheben. Für sicherheitskritische Szenarien existieren Ansätze wie gVisor oder Kata Containers, die zusätzliche Isolationsschichten einführen.
Abgrenzung zu virtuellen Maschinen
Virtuelle Maschinen (VMs) emulieren vollständige Hardware. Jede VM enthält ein eigenes Betriebssystem mit Kernel, Treibern und Systemdiensten. Das erzeugt starke Isolation, kostet aber Ressourcen. Eine typische VM belegt mehrere Gigabyte Festplattenspeicher und benötigt 30 Sekunden bis mehrere Minuten zum Starten.
Container teilen sich den Kernel des Host-Systems. Sie enthalten nur die Anwendungsschicht. Dadurch sind sie deutlich kleiner (oft 50 bis 500 MB), starten in Millisekunden bis wenigen Sekunden und erlauben eine höhere Dichte pro Server.
Beispiel: Auf einem Server mit 64 GB RAM lassen sich etwa 8 bis 10 VMs mit je eigenem Betriebssystem betreiben. Derselbe Server kann 50 bis 100 Container tragen, weil der Kernel-Overhead entfällt.
Beide Ansätze schließen sich nicht aus. In der Praxis laufen Container häufig innerhalb von VMs. Die VM liefert die Hardware-Isolation, der Container die Anwendungs-Portabilität.
Orchestrierung: Container im Verbund steuern
Ein einzelner Container löst das Verpackungsproblem. In der Praxis bestehen Anwendungen aus vielen Diensten: Webserver, Datenbank, Cache, Hintergrundverarbeitung. Diese Dienste laufen in separaten Containern und müssen koordiniert werden.
Orchestrierungswerkzeuge übernehmen diese Aufgabe. Sie starten Container auf verfügbaren Rechnern, überwachen ihren Zustand, ersetzen ausgefallene Instanzen und verteilen eingehende Anfragen. Kubernetes hat sich als Standard für diese Aufgabe etabliert.
Beispiel: Ein Online-Shop besteht aus einem Frontend-Container, einem Katalog-Service, einem Bestell-Service und einer Datenbank. Kubernetes verteilt diese Container auf einen Cluster aus fünf Servern. Fällt ein Server aus, werden die betroffenen Container automatisch auf den verbleibenden Servern neu gestartet.
Beispiel: Am Black Friday verzehnfacht sich die Last eines Webshops. Kubernetes skaliert die Frontend-Container von 3 auf 30 Instanzen. Nach dem Lastspitze reduziert es die Anzahl automatisch zurück.
Für kleinere Setups mit wenigen Containern auf einem einzelnen Rechner reicht Docker Compose. Es beschreibt alle Dienste in einer YAML-Datei und startet sie mit einem einzigen Befehl.
Containerisierung im Machine-Learning-Kontext
Im Bereich Machine Learning verschärft sich das Abhängigkeitsproblem. Modelle benötigen spezifische Versionen von Frameworks (TensorFlow, PyTorch), GPU-Treibern (CUDA), numerischen Bibliotheken (cuDNN, BLAS) und Datenverarbeitungswerkzeugen. Schon kleine Versionsabweichungen können Ergebnisse verändern oder Trainingsabbrueche verursachen.
Beispiel: Ein Deep-Learning-Modell wurde mit PyTorch 2.1 und CUDA 12.1 trainiert. Auf einem Server mit CUDA 11.8 schlägt der Inferenz-Aufruf fehl. Ein Container mit den exakten Versionen löst das Problem.
Reproduzierbarkeit ist in der ML-Forschung ein anerkanntes Problem. Container dokumentieren die gesamte Laufzeitumgebung in einer Datei. Jedes Experiment kann Monate später exakt wiederholt werden, wenn das Image archiviert wird.
Beispiel: Ein Forschungsteam veröffentlicht ein Paper mit zugehörigem Docker-Image. Andere Forscherinnen können die Ergebnisse reproduzieren, ohne die Umgebung manuell nachzubauen.
GPU-Zugriff aus Containern wird durch die NVIDIA Container Runtime ermöglicht. Sie reicht GPU-Geräte in den Container durch, ohne dass GPU-Treiber im Container selbst installiert sein müssen.
Erprobte Praktiken und typische Fallstricke
Einige Muster haben sich in der Container-Praxis als zuverlässig erwiesen. Ebenso gibt es wiederkehrende Fehler.
Images klein halten
Große Images verlängern Build-Zeiten, Transfers und Startzeiten. Alpine-basierte Images oder Multi-Stage-Builds reduzieren die Größe erheblich.
Beispiel: Ein Python-Image auf Basis von python:3.11 ist etwa 900 MB groß. Mit python:3.11-slim schrumpft es auf 150 MB. Ein Multi-Stage-Build, der nur die fertig kompilierte Anwendung in ein minimales Image kopiert, erreicht unter 50 MB.
Kein Zustand im Container
Container sind kurzlebig. Daten, die nach einem Neustart noch vorhanden sein müssen, gehören in externe Volumes oder Datenbanken. Persistente Daten im Container-Dateisystem gehen beim Entfernen des Containers verloren.
Beispiel: Eine Webanwendung speichert hochgeladene Dateien im lokalen Dateisystem des Containers. Nach einem Update wird der Container ersetzt. Alle Uploads sind weg. Lösung: Ein externes Volume oder ein Objektspeicher wie S3.
Sicherheitsupdates nicht vernachlässigen
Container-Images frieren den Stand der enthaltenen Pakete ein. Eine bekannte Schwachstelle in einer Bibliothek bleibt bestehen, bis das Image neu gebaut wird. Automatisierte Image-Scans (Trivy, Grype) und regelmäßige Rebuilds sind notwendig.
Grenzen und Einordnung
Containerisierung ist kein Allheilmittel. Es gibt klar definierte Situationen, in denen der Ansatz an Grenzen stößt.
Anwendungen, die tief in das Betriebssystem eingreifen (Kernelmodule, spezielle Gerätetreiber), lassen sich nicht sinnvoll containerisieren. Der geteilte Kernel setzt voraus, dass die Anwendung mit den Standardschnittstellen des Betriebssystems auskommt.
GUI-Anwendungen sind in Containern möglich, aber umständlich. X11- oder Wayland-Forwarding erfordert zusätzliche Konfiguration und hebt Teile der Isolation auf.
Die Lernkurve ist nicht zu unterschätzen. Dockerfile-Syntax, Image-Schichtung, Netzwerkkonfiguration, Volume-Management und Orchestrierung bilden ein eigenes Wissensgebiet. Für DevOps-Teams ist dieses Wissen inzwischen vorausgesetzt. Für reine Fachanwenderinnen kann der Einstieg aufwändig sein.
Ressourcen-Overhead existiert, auch wenn er kleiner ist als bei VMs. Jeder Container beansprucht Dateisystem-Schichten, Netzwerkbrücken und Prozessverwaltung. Bei sehr vielen kleinen Containern summiert sich das.
Fachliche Einordnung: Containerisierung hat seit etwa 2013 (Veröffentlichung von Docker) die Art verändert, wie Software ausgeliefert wird. Der Ansatz ist keine Erfindung von Docker. Unix-Systeme kannten chroot seit 1979, FreeBSD Jails seit 2000, Linux-Namespaces seit 2002. Docker hat diese Mechanismen zugänglich gemacht und mit einem Ökosystem aus Registries und Tooling versehen. Die Technologie ist ausgereift und Industriestandard, ersetzt aber nicht die Notwendigkeit, Anwendungsarchitektur, Netzwerksicherheit und Betriebsprozesse sorgfältig zu planen.