- Selbst wenn man
docker run ubuntu ausführt, wird der Linux-Kernel des Hosts gemeinsam genutzt, und Ubuntu liefert nur User-Space-Werkzeuge
- Das Ergebnis von
uname -r zeigt die Kernel-Version des Hosts, während nur /etc/os-release Ubuntu-Informationen anzeigt
- VMs besitzen jeweils einen eigenen Kernel und benötigen mehrere Minuten zum Booten, während Container innerhalb von Millisekunden starten und sich den Host-Kernel über Isolierung auf OS-Ebene ohne Hardware-Virtualisierung teilen, was den Overhead gering hält
- Dank der Stabilität der Linux-System-Call-ABI können Container verschiedener Distributionen auf demselben Kernel laufen
- In einer Umgebung mit 16 GB RAM liegt die praktische Obergrenze bei etwa 50–100 leichten Containern, 10–30 mittelgroßen und 5–10 großen Containern
- Dieses Architekturverständnis ist wichtig, weil Kernel-Schwachstellen alle Container betreffen und die Wahl des Basis-Images direkte Auswirkungen auf Kompatibilität und Sicherheit hat
Was es bedeutet, „Ubuntu auszuführen“
- Führt man
docker run ubuntu:22.04 aus, erhält man einen Bash-Prompt, der wie Ubuntu aussieht, und kann apt update sowie Paketinstallationen ausführen
- Führt man jedoch innerhalb des Containers
uname -r aus, wird die Kernel-Version des Hosts angezeigt (z. B. 6.5.0-44-generic)
- Die Datei
/etc/os-release zeigt zwar Ubuntu 22.04 an, aber der Kernel gehört zum Host-System, und der „Ubuntu“-Teil ist lediglich das Dateisystem, das den User Space bildet
Container vs. virtuelle Maschinen: Architekturvergleich
- VMs virtualisieren die Hardware, Container virtualisieren das Betriebssystem
- Wichtige Unterschiede:
- Kernel: VMs besitzen jeweils einen eigenen Kernel, Container teilen sich den Host-Kernel
- Boot-Zeit: VMs mehrere Minuten, Container Millisekunden
- Speicher-Overhead: VMs 512 MB–4 GB, Container 1–10 MB
- Festplattennutzung: VMs 10–100 GB, Container-Images 10–500 MB
- Isolierungsgrad: VMs auf Hardware-Ebene, Container auf Prozess-Ebene
- Leistung: VMs mit etwa 5–10 % Overhead, Container mit nahezu nativer Performance
Woraus ein Basis-Image tatsächlich besteht
- Inhalt des Tarballs, der beim Pullen von
ubuntu:22.04 heruntergeladen wird:
-
1. Unverzichtbare Binärdateien (/bin, /usr/bin)
/bin/bash (Shell), /bin/ls (Dateiliste), /bin/cat (Dateianzeige)
/usr/bin/apt (Paketmanager), /usr/bin/dpkg (Debian-Paketwerkzeug)
-
2. Gemeinsame Bibliotheken (/lib, /usr/lib)
- glibc und weitere Shared Libraries, gegen die Programme gelinkt sind
/lib/x86_64-linux-gnu/libc.so.6 (C-Bibliothek – Grundlage aller C-Programme)
- Wichtige Bibliotheken wie
libpthread.so.0, libm.so.6 usw.
-
3. Konfigurationsdateien (/etc)
/etc/apt/sources.list (Paket-Repositories)
/etc/passwd (Benutzerdatenbank)
/etc/resolv.conf (DNS-Konfiguration, meist vom Host gemountet)
-
4. Paketdatenbank
/var/lib/dpkg/status (installierte Pakete)
/var/lib/apt/lists/ (Cache verfügbarer Pakete)
- Kernel, Bootloader und Treiber sind nicht enthalten
Der Kernel bleibt gleich, alles andere ändert sich
- Funktionen, die der Linux-Kernel bereitstellt: Prozess-Scheduling, Speicherverwaltung, Dateisystem-Operationen, Netzwerk-Stack, Gerätetreiber, System Calls
- Wenn ein Container-Prozess
open(), read(), fork() aufruft, wird dies direkt an den Host-Kernel weitergereicht
- Dem Kernel ist weder bekannt noch wichtig, ob der betreffende Prozess in einem „Ubuntu-Container“ oder „Alpine-Container“ läuft
-
Stabilität der System-Call-Schnittstelle
- Die Linux-Syscall-ABI ist sehr stabil
- Warum ein mit glibc 2.31 (Ubuntu 20.04) kompiliertes Binary auch auf einem Ubuntu-24.04-Kernel läuft:
- Der Kernel wahrt Abwärtskompatibilität
- Keine Änderung der System-Call-Nummern
- Neue Funktionen kommen hinzu, bestehende werden aber kaum entfernt
- Deshalb kann ein Ubuntu-18.04-Container auf einem Host mit Kernel 6.5 laufen
Praktischer Test: gleicher Kernel, anderer User Space
- Führt man dieselbe Kernel-Abfrage in mehreren Basis-Images aus, sieht man, dass alle Images den Host-Kernel gemeinsam nutzen
ubuntu:22.04, debian:12, alpine:3.19, fedora:39, archlinux:latest zeigen alle dieselbe Kernel-Version an (6.5.0-44-generic)
- Unterschiede zwischen den Containern liegen bei Komponenten wie dem
uname-Binary oder libc, also in der Userland-Zusammensetzung
Warum Container so effizient sind
-
1. Keine Kernel-Duplizierung
- VMs laden jeweils einen vollständigen Kernel in den Speicher (ca. 100–500 MB)
- 10 VMs verbrauchen Speicher für 10 Kernel, 10 Container nutzen nur einen Kernel
-
2. Sofortiger Start
- Boot-Reihenfolge einer VM: BIOS → Bootloader → Kernel → Init-System → Services
- Ein Container existiert mit nur
fork()- und exec()-Aufrufen innerhalb von Millisekunden als Prozess
- Typischer VM-Start: 30–60 Sekunden / Container-Start: etwa 0,347 Sekunden
-
3. Gemeinsame Image-Layer
- Werden 100 Container aus
ubuntu:22.04 gestartet, existieren die Basis-Image-Layer nur einmal auf der Festplatte
- Jeder Container erhält lediglich einen dünnen Copy-on-Write-Layer für Änderungen
-
4. Speicherteilung über den Kernel
- Der Page Cache des Kernels wird gemeinsam genutzt
- Wenn 50 Container dieselbe Datei lesen, cached der Kernel sie nur einmal
- Bei identischen Shared Libraries können Speicherseiten per Copy-on-Write gemeinsam genutzt werden
Berechnung der Container-Grenzen
-
Speicheranalyse (auf Basis einer VM mit 16 GB RAM)
- Gesamter RAM: 16.384 MB
- Overhead des Host-OS: -1.024 MB
- Docker-Daemon: -256 MB
- Overhead der Container-Runtime: -512 MB
- Für Container verfügbar: 14.592 MB
-
Speicherverbrauch nach Container-Typ
- Minimal (
sleep): ca. 1 MB
- Alpine + kleine App: ca. 25 MB
- Ubuntu + Python-App: ca. 120 MB
- Ubuntu + Java-App: ca. 500 MB
- Node.js-Service: ca. 200 MB
-
Theoretisches Maximum
- Minimal-Container (1 MB): 14.592
- Alpine + kleine App (25 MB): 583
- Ubuntu + Python (120 MB): 121
- Java-Microservice (500 MB): 29
-
Praktische Grenzen
- Zusätzlich zum Speicher zu beachten:
- CPU-Scheduling: Zu viele konkurrierende Container verursachen Latenzspitzen
- Dateideskriptoren: Standard-
ulimit 1024
- Netzwerk-Ports: Für Port-Mapping stehen nur 65.535 zur Verfügung
- PIDs: Begrenzung durch
/proc/sys/kernel/pid_max (Standard: 32.768)
- Festplatten-I/O: OverlayFS-Overhead, viele Layer müssen durchsucht werden
- Bei realen Workloads auf einer VM mit 16 GB liegt die praktische Obergrenze bei:
- Leichten Containern (API, Worker): 50–100
- Mittleren Containern (DB, Cache): 10–30
- Großen Containern (ML-Modelle, JVM-Apps): 5–10
Linux-Distributionskompatibilität
-
ABI-Zusicherung des Kernels
- Linux hält eine stabile Syscall-Schnittstelle aufrecht
- Für alte Kernel kompilierte Binaries laufen auf neuen Kerneln
- Ein Ubuntu-18.04-Binary läuft problemlos auf Kernel 6.5
-
Wann die Kompatibilität bricht
- Anforderungen an Kernel-Funktionen: wenn der Container Features braucht, die der Kernel nicht hat (z. B. io_uring erfordert Kernel 5.1+)
- Abhängigkeit von Kernel-Modulen: WireGuard benötigt das WireGuard-Kernelmodul, NVIDIA-Container benötigen den NVIDIA-Kerneltreiber
- Seccomp-/Capability-Beschränkungen: wenn der Host benötigte Syscalls blockiert (z. B. erfordert die Nutzung von
ptrace --cap-add SYS_PTRACE)
Leitfaden zur Wahl des Basis-Images
| Basis-Image |
Größe |
Paketmanager |
Verwendungszweck |
scratch |
0 MB |
keiner |
statisch kompilierte Go-/Rust-Binaries |
alpine |
7 MB |
apk |
minimale Container, musl libc |
distroless |
20 MB |
keiner |
sicherheitsorientiert, ohne Shell und Paketmanager |
debian-slim |
80 MB |
apt |
ausgewogen zwischen Größe und Kompatibilität |
ubuntu |
78 MB |
apt |
entwicklerfreundlich |
fedora |
180 MB |
dnf |
aktuelle Pakete, SELinux |
-
Wann welches Image sinnvoll ist
- scratch: für statisch kompilierte Binaries, enthält nur das Binary und sonst kein OS
- alpine: minimales Image mit Shell-Zugriff; nutzt musl libc statt glibc, was zu Kompatibilitätsproblemen führen kann
- distroless: sicherheitsorientiertes Produktions-Image; Debugging ist schwieriger, weil Shell und Paketmanager fehlen, dafür ist es sicherer
Grenze zwischen User Space und Kernel
-
Was aus dem Basis-Image kommt (User Space)
- Shell (
/bin/bash, /bin/sh)
- C-Bibliotheken (glibc, musl)
- Paketmanager (apt, apk, yum)
- Zentrale Utilities (ls, cat, grep)
- Konfiguration des Init-Systems (meist nicht systemd selbst)
- Standardbenutzer und -gruppen (
/etc/passwd)
- Bibliothekspfade und Konfiguration
-
Was vom Host kommt (Kernel)
- Prozess-Scheduling und Speicherverwaltung
- Netzwerk-Stack (TCP/IP, Routing)
- Dateisystem-Operationen (Lesen, Schreiben, Mounten)
- Sicherheitsfunktionen (Namespaces, cgroups, seccomp)
- Gerätetreiber (GPU, Netzwerk, Storage)
- Zeit- und Taktverwaltung
- Kryptografie und Zufallszahlengenerierung
-
Die durch Namespaces erzeugte Illusion
- Der Kernel stellt Namespaces bereit, sodass sich Container isoliert anfühlen
- Ein Prozess, der im Container als PID 1 erscheint, existiert auf dem Host unter einer höheren PID (z. B. 45678)
- Der Kernel hält die Zuordnung aufrecht: Container-PID 1 → Host-PID 45678
- So funktioniert Isolierung ohne Virtualisierung
Bedeutung für Produktionsumgebungen
-
1. Kernel-Schwachstellen betreffen alle Container
- Hat der Host-Kernel eine Schwachstelle, sind alle Container exponiert
- Host-Patches aktuell zu halten ist Pflicht
-
2. Der Host-Kernel begrenzt die Container-Funktionen
- Für die Nutzung von io_uring ist auf dem Host Kernel 5.1+ nötig
- eBPF-Funktionen erfordern Kernel 4.15+ mit bestimmten aktivierten Optionen
-
3. Bedeutung von glibc vs. musl
- Alpine verwendet musl libc
- Manche für glibc kompilierten Binaries funktionieren dort nicht
- Beispiel: Beim Ausführen eines glibc-Binarys auf Alpine kann ein Fehler auftreten, weil
/lib/x86_64-linux-gnu/libc.so.6 nicht vorhanden ist
-
4. Das Container-„OS“ ist rein ein Organisationskonzept
- Aus Sicht des Kernels gibt es keinen Unterschied zwischen einem „Ubuntu-Container“ und einem „Debian-Container“
- Beides sind lediglich Prozesse, die Syscalls ausführen
Häufige Missverständnisse
- ❌ „Container sind leichte VMs“: Container sind Prozesse mit fortgeschrittener Isolierung; VMs virtualisieren Hardware und führen einen separaten Kernel aus
- ❌ „Jeder Container hat seinen eigenen Kernel“: Alle Container teilen sich den Host-Kernel; das „OS“ des Containers besteht nur aus User-Space-Dateien
- ❌ „Einen Ubuntu-Container starten = Ubuntu ausführen“: Man nutzt den Host-Kernel und Ubuntu-Werkzeuge; wenn der Host Debian ist, läuft tatsächlich ein Debian-Kernel
- ❌ „Ein Basis-Image enthält ein vollständiges Betriebssystem“: Ein Basis-Image enthält nur minimale User-Space-Werkzeuge, keinen Kernel, Bootloader oder Treiber
- ❌ „Mehr Container = mehr Speicher“: Durch gemeinsame Layer und Kernel-Page-Caching können Container Speicher oft effizient gemeinsam nutzen
Kernaussagen
- Ein Docker-Basis-Image ist ein Dateisystem-Snapshot der User-Space-Komponenten einer Linux-Distribution
- Also der Binärdateien, Bibliotheken und Konfigurationen, durch die sich Ubuntu wie Ubuntu anfühlt
- Das eigentliche Betriebssystem, also der Kernel, wird mit dem Host geteilt
- Diese Architektur ermöglicht:
- Startzeiten im Millisekundenbereich (kein Kernel-Boot)
- Minimalen Speicher-Overhead (ein Kernel, gemeinsam genutzte Seiten)
- Hohe Dichte im großen Maßstab (Hunderte Container pro Host)
- Nahezu native Performance (direkte Syscalls an den Kernel)
- Der Trade-off ist eine schwächere Isolierung als bei VMs – da Container den Kernel teilen, betreffen Kernel-Exploits alle Container
- Für die meisten Workloads ist dieser Trade-off lohnend
Noch keine Kommentare.