cgroups, OOM-Killer und Compose-Konfiguration erklärt
Ein einzelner Container ohne Limits kann den gesamten Host in die Knie zwingen. Wer Docker CPU und Memory Limits kennt, verhindert Ressourcenmonopole, macht Kapazitätsplanung messbar und schützt alle Services auf demselben Host – nicht nur den eigenen.
Inhaltsverzeichnis
- 1. Warum Docker Memory Limits keine optionale Spielerei sind
- 2. cgroups v1 und v2: Was Docker darunter wirklich tut
- 3. Memory-Limits konfigurieren: --memory, --memory-swap und --memory-reservation
- 4. CPU-Limits konfigurieren: --cpus, --cpu-shares und --cpuset-cpus
- 5. Ressourcenlimits in Docker Compose definieren
- 6. OOM-Situationen erkennen und diagnostizieren
- 7. Sinnvolle Grenzwerte für PHP, Node und Datenbank-Container ermitteln
- 8. Ressourcenverbrauch laufend beobachten
- 9. Konfigurationsvarianten im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Docker Memory Limits keine optionale Spielerei sind
Ein Container ohne konfigurierte Docker Memory Limits kann im Prinzip unbegrenzt viel RAM des Hosts belegen. Das mag in einer Entwicklungsumgebung harmlos wirken, ist aber auf einem geteilten Produktionshost ein ernstes Problem. Wächst der Speicherbedarf eines einzigen Containers unkontrolliert – zum Beispiel weil eine Schleife einen Memory-Leak produziert oder ein unkontrollierter Import läuft – beginnt der Linux-Kernel, Prozesse zu beenden. Der OOM-Killer wählt dabei nicht zwingend den verantwortlichen Prozess, sondern den, der am meisten Speicher belegt. Das kann der Datenbankprozess eines anderen Containers sein.
Dasselbe gilt für CPU. Ohne Docker CPU Limits kann ein rechenintensiver Container alle verfügbaren Kerne über längere Zeit monopolisieren. Andere Container, die auf demselben Host laufen – etwa Nginx, Redis oder der PHP-FPM-Prozess der Hauptanwendung – bekommen in dieser Zeit kaum CPU-Zeit zugeteilt und reagieren für Endnutzer spürbar langsam oder gar nicht. Wer Docker CPU und Memory Limits setzt, schafft klare, vorhersagbare Kapazitätsgrenzen pro Service und macht den Host als Ganzes stabiler.
2. cgroups v1 und v2: Was Docker darunter wirklich tut
Docker nutzt Linux Control Groups (cgroups), um Ressourcen pro Container zu begrenzen. Wenn man mit docker run --memory 512m einen Container startet, legt Docker intern eine cgroup-Hierarchie an und schreibt den Grenzwert in das entsprechende cgroup-Subsystem. Der Kernel erzwingt dieses Limit dann ohne weiteres Zutun der Container-Laufzeitumgebung. Mit dem Wechsel zu cgroups v2 – standardmäßig aktiv ab Linux 5.10 und in modernen Distributionen wie Ubuntu 22.04 – wurden die Subsysteme unified: statt separater Verzeichnisse für memory, cpu und blkio gibt es nun eine einheitliche Schnittstelle unter /sys/fs/cgroup/.
Docker 24 und höher unterstützt cgroups v2 vollständig. Für Nutzer ändert sich die Konfiguration nicht, aber die Kernel-seitige Durchsetzung ist konsistenter. Ein wichtiger Unterschied: Mit cgroups v2 können Docker Memory Limits auch für Kernel-Speicher und Memory-mapped Dateien präziser durchgesetzt werden. Der Befehl cat /proc/cgroups zeigt, welche Subsysteme aktiv sind, und stat -fc %T /sys/fs/cgroup/ verrät, ob cgroups v2 aktiv ist – die Ausgabe lautet dann cgroup2fs.
# Check whether cgroups v2 is active on the host
stat -fc %T /sys/fs/cgroup/
# Output "cgroup2fs" = v2, "tmpfs" = v1
# Inspect the cgroup limits Docker set for a running container
CONTAINER_ID=$(docker inspect --format '{ {.Id} }' my-app)
# cgroups v2 path
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.max
# Docker's own view of resource constraints
docker inspect --format '{ {.HostConfig.Memory} } bytes, { {.HostConfig.NanoCpus} } NanoCPUs' my-app
# Live resource usage of all running containers
docker stats --no-stream --format "table { {.Name} }\t{ {.CPUPerc} }\t{ {.MemUsage} }\t{ {.MemPerc} }"
Wichtig: Docker Memory Limits begrenzen nur den Userspace-Speicher des Containers, nicht automatisch den Swap. Ohne explizites --memory-swap kann ein Container standardmäßig denselben Betrag nochmals als Swap nutzen – ein Container mit --memory 512m kann also bis zu 1 GiB Speicher (RAM + Swap) belegen. Wer das verhindern will, muss --memory-swap explizit auf denselben Wert wie --memory setzen.
3. Memory-Limits konfigurieren: --memory, --memory-swap und --memory-reservation
Der Parameter --memory (oder -m) setzt das harte Limit für den Arbeitsspeicher eines Containers. Überschreitet der Container diesen Wert, greift der OOM-Killer und beendet Prozesse innerhalb des Containers – standardmäßig den Prozess mit dem höchsten RSS (Resident Set Size). Mit --memory-swap kontrolliert man das kombinierte RAM- und Swap-Limit. Setzt man beide auf denselben Wert, ist kein Swap erlaubt. Setzt man --memory-swap -1, darf der Container unbegrenzt viel Swap nutzen, was selten sinnvoll ist. Die Differenz zwischen --memory-swap und --memory ergibt die erlaubte Swap-Nutzung.
Das Soft-Limit --memory-reservation ist ein weicherer Grenzwert. Docker erzwingt ihn nicht aktiv, aber wenn der Host unter Speicherdruck gerät, versucht das System, den Speicherverbrauch der Container auf das Reservation-Level zu drücken. Das ist nützlich für Services, die normalerweise wenig RAM brauchen, aber kurzzeitig mehr benötigen dürfen – solange genug frei ist. Die Kombination aus --memory 1g und --memory-reservation 512m bedeutet: das harte Limit liegt bei 1 GiB, aber unter Host-Druck wird angestrebt, den Container auf 512 MiB zu beschränken.
4. CPU-Limits konfigurieren: --cpus, --cpu-shares und --cpuset-cpus
Der Parameter --cpus ist die modernste und direkteste Art, Docker CPU Limits zu setzen. Mit --cpus 0.5 darf der Container maximal die Hälfte eines CPU-Kerns nutzen, mit --cpus 2.0 bis zu zwei vollständige Kerne. Intern setzt Docker damit die cgroup-Parameter cpu.cfs_quota_us und cpu.cfs_period_us. Das ist ein hartes Throttling: überschreitet der Container die Quote, wird er für den Rest der Periode nicht mehr eingeplant – auch wenn andere Kerne vollständig leer laufen.
Die Option --cpu-shares funktioniert anders: Sie setzt keine absolute Obergrenze, sondern eine relative Gewichtung. Mit dem Standardwert 1024 bekommt jeder Container denselben Anteil. Ein Container mit 2048 bekommt doppelt so viel CPU-Zeit wie einer mit 1024, aber nur dann, wenn Contention besteht. Wenn die CPU frei ist, darf jeder Container unbegrenzt rechnen. Das macht --cpu-shares nützlich für Prioritätsvergabe, aber nicht für hartes Ressourcenlimiting. --cpuset-cpus pinnt einen Container auf bestimmte CPU-Kerne – sinnvoll für NUMA-Architekturen oder latenzempfindliche Services, die keinen CPU-Cache-Thrash durch andere Prozesse vertragen.
# Hard CPU limit: max 1.5 cores, max 512 MiB RAM, no swap allowed
docker run -d \
--name php-fpm \
--cpus 1.5 \
--memory 512m \
--memory-swap 512m \
--memory-reservation 256m \
my-php-fpm:latest
# Priority-based sharing: db gets twice the CPU weight vs. app
docker run -d --name db --cpu-shares 2048 mysql:8.4
docker run -d --name app --cpu-shares 1024 my-app:latest
# Pin container to cores 0 and 1 (NUMA / cache isolation)
docker run -d --cpuset-cpus "0,1" --memory 2g my-compute:latest
# Verify actual cgroup quota written by Docker
docker inspect php-fpm | grep -E '"NanoCpus"|"Memory"'
# NanoCpus: 1500000000 = 1.5 CPUs, Memory: 536870912 = 512 MiB
5. Ressourcenlimits in Docker Compose definieren
In Docker Compose v2 werden Docker CPU und Memory Limits unter dem Schlüssel deploy.resources konfiguriert. Das spiegelt die Swarm-Syntax wider, gilt aber seit Compose v2 auch für einfache docker compose up-Deployments ohne Swarm. Der Schlüssel limits setzt harte Obergrenzen, der Schlüssel reservations die weichen Mindestreservierungen. Bei reservations ist wichtig zu verstehen, dass Docker Compose diesen Wert als Soft-Hint an den Kernel übermittelt – er garantiert keinen dedizierten Speicher, beeinflusst aber das Scheduling unter Host-Druck.
Wer mehrere Umgebungen mit unterschiedlichen Ressourcengrenzen betreibt – etwa schmalere Limits für CI und großzügigere für Produktion – kann in Compose-Dateien über Environment-Variablen parametrisieren. ${PHP_MEMORY_LIMIT:-512m} in der Compose-Datei liest den Wert aus der Umgebung und fällt auf 512m zurück, wenn die Variable nicht gesetzt ist. Das ermöglicht eine einzige Compose-Datei für alle Umgebungen, ohne Werte hardzucoden.
# docker-compose.yml — resource limits for a PHP/MySQL stack (Compose v2 syntax)
services:
php-fpm:
image: my-php-fpm:8.4
deploy:
resources:
limits:
cpus: "1.5"
memory: ${PHP_MEMORY_LIMIT:-512m}
reservations:
cpus: "0.25"
memory: 256m
mysql:
image: mysql:8.4
deploy:
resources:
limits:
cpus: "2.0"
memory: 2g
reservations:
cpus: "0.5"
memory: 1g
environment:
# Tell MySQL to stay within Docker memory limits
MYSQL_INNODB_BUFFER_POOL_SIZE: "1073741824" # 1 GiB
redis:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
deploy:
resources:
limits:
cpus: "0.5"
memory: 192m
6. OOM-Situationen erkennen und diagnostizieren
Wenn ein Container durch den OOM-Killer beendet wird, erscheint er mit dem Status Exited (137) – Exit-Code 137 entspricht Signal 9 (SIGKILL). Das ist das erste Indiz für eine OOM-Situation. Zur Bestätigung lohnt sich ein Blick in das Kernel-Journal: dmesg | grep -i "oom\|killed" zeigt die OOM-Einträge mit Zeitstempel, betroffenen Prozessen und dem Meminfo-Snapshot des Augenblicks. Dieser Eintrag enthält auch, wie hoch das konfigurierte Docker Memory Limit war und wie viel der Container tatsächlich belegte.
Ein subtileres Problem ist Swap-Thrashing: Der Container überschreitet das RAM-Limit, nutzt Swap und wird dadurch extrem langsam, ohne sofort zu sterben. Das erkennt man an der Metrik MemUsage in docker stats, die sich dem Limit nähert, und an einem massiven Anstieg der Disk-I/O-Latenz. In diesem Fall ist die richtige Reaktion nicht, das Memory-Limit zu erhöhen, sondern zunächst das Speicherprofil des Containers unter Reallast zu messen – denn oft liegt ein echtes Problem im Anwendungscode vor, das durch mehr RAM nur versteckt wird.
7. Sinnvolle Grenzwerte für PHP, Node und Datenbank-Container ermitteln
Die häufigste Frage bei Docker Memory Limits ist: Welchen Wert setze ich? Eine sinnvolle Methode ist das Profiling unter realistischer Last. Man startet den Container zunächst ohne Limits, lässt typische Last darüber laufen und beobachtet den Peak-Speicherverbrauch mit docker stats über mehrere Minuten. Dann setzt man das Limit auf das 1,5-fache des gemessenen Peaks – als Puffer für kurzfristige Spitzen. Wer direkt mit dem theoretischen Maximum beginnt, setzt Limits oft zu eng und erzeugt OOM-Kills unter normalem Betrieb.
Für PHP-FPM-Container gilt als Faustregel: Speicherbedarf pro Worker-Prozess multipliziert mit der Anzahl der konfigurierten Worker, plus 10–20% Overhead. Bei 10 Workern mit je 40 MiB kommt man auf etwa 440–480 MiB als Limit. Für Node.js-Container ist --max-old-space-size der kritische Parameter – er muss explizit gesetzt sein, sonst kennt der Garbage Collector das Docker-Limit nicht und überschreitet es. MySQL und MariaDB sollten mit dem Parameter innodb_buffer_pool_size so konfiguriert werden, dass Buffer-Pool plus Overhead das Docker Memory Limit nicht überschreiten – sonst droht ein OOM-Kill ausgerechnet im schlimmsten Moment.
# Profile memory peak under realistic load (sample every 2s for 5 min)
docker stats --no-stream --format "{ {.MemUsage} }" php-fpm
# Run load test in parallel, then read peak from stats output
# Node.js: inform V8 garbage collector about the memory limit
docker run -d \
--name node-app \
--memory 512m \
--memory-swap 512m \
-e NODE_OPTIONS="--max-old-space-size=400" \
my-node:20-alpine
# PHP-FPM: calculate per-worker memory and set limit accordingly
# pm.max_children = 10, ~40 MiB each => set limit to 512m with buffer
docker run -d \
--name php-fpm \
--memory 512m \
--memory-swap 512m \
-e PHP_MEMORY_LIMIT=256M \
my-php-fpm:8.4
# Check OOM events in kernel log
dmesg --time-format iso | grep -i "out of memory\|oom_kill\|killed process" | tail -20
8. Ressourcenverbrauch laufend beobachten
Das eingebaute docker stats liefert Echtzeit-Metriken: CPU-Prozentsatz, RAM-Nutzung mit Limit, Netzwerk-I/O und Block-I/O. Das genügt für manuelle Kontrolle, reicht aber nicht für Alerting und historische Auswertung. Für Produktionsumgebungen empfiehlt sich die Integration mit Prometheus und cAdvisor: cAdvisor exportiert alle Docker-Metriken in einem Prometheus-kompatiblen Format und macht sie für Grafana-Dashboards nutzbar. Besonders relevante Metriken sind container_memory_usage_bytes im Verhältnis zu container_spec_memory_limit_bytes sowie container_cpu_cfs_throttled_seconds_total, das zeigt, wie oft der Container durch das Docker CPU Limit gebremst wurde.
Ein einfaches Frühwarnsystem ohne Prometheus: ein Cron-Job, der docker stats --no-stream auswertet und eine Warnung schickt, wenn ein Container mehr als 80% seines Docker Memory Limits belegt. Das gibt Zeit zum Reagieren, bevor der OOM-Killer eingreift. docker events liefert darüber hinaus Echtzeit-Events des Docker-Daemons, darunter auch OOM-Events, die sich mit --filter event=oom direkt filtern lassen.
9. Konfigurationsvarianten im Vergleich
Die verschiedenen Ressourcenparameter haben unterschiedliche Wirkungsweisen und Einsatzbereiche. Die Wahl des richtigen Parameters für Docker CPU und Memory Limits hängt davon ab, ob man ein hartes Maximum, eine relative Priorisierung oder eine weiche Reservierung benötigt.
| Parameter | Art | Wirkung | Typischer Einsatz |
|---|---|---|---|
--memory |
Hartes Limit | OOM-Kill wenn überschritten | Alle produktiven Container |
--memory-swap |
Hartes Limit | RAM + Swap kombiniert | Swap vollständig deaktivieren |
--memory-reservation |
Soft-Limit | Ziel unter Host-Druck | Services mit variablem Bedarf |
--cpus |
Hartes Limit | Throttling per CFS-Quota | Rechenintensive Container |
--cpu-shares |
Relatives Gewicht | Priorität bei Contention | Priorisierung ohne hartes Cap |
In der Praxis empfiehlt sich die Kombination aus hartem --memory-Limit und einem --memory-reservation-Wert, der etwa 50–60% des harten Limits beträgt. Das gibt dem Container Raum für kurzfristige Spitzen, signalisiert dem Kernel aber gleichzeitig, welchen Baseline-Bedarf der Container hat. Für CPU reicht in den meisten Web-Service-Szenarien --cpus mit einem realistischen Wert – ein PHP-FPM-Container mit 8 Workern hat typischerweise keine Notwendigkeit, mehr als 2 Kerne zu beanspruchen.
Mironsoft
Docker-Infrastruktur, Ressourcenplanung und Produktions-Deployments
Docker-Stack ohne Ressourcenkonflikte?
Wir analysieren bestehende Container-Stacks, ermitteln reale Ressourcenprofile unter Last und setzen sinnvolle Docker Memory Limits und CPU-Grenzen – damit kein Container mehr den gesamten Host destabilisiert.
Ressourcen-Profiling
Peak-Messung unter realistischer Last und Ableitung sinnvoller Limits pro Service
OOM-Analyse
Diagnose bestehender OOM-Situationen und Behebung der Ursachen im Anwendungscode
Monitoring-Setup
cAdvisor + Prometheus + Grafana für kontinuierliche Ressourcenüberwachung konfigurieren
10. Zusammenfassung
Docker CPU und Memory Limits sind kein optionales Feature, sondern Voraussetzung für stabile Produktionsumgebungen mit mehreren Containern auf demselben Host. Der Kernel erzwingt diese Limits über cgroups – Docker übersetzt die --memory- und --cpus-Parameter direkt in cgroup-Konfigurationen. Hartes --memory-Limit verhindert, dass ein Container unbegrenzt RAM belegt und den OOM-Killer auf kritische Prozesse lenkt. --memory-swap auf denselben Wert wie --memory deaktiviert Swap vollständig und vermeidet unsichtbares Swap-Thrashing. --cpus throttelt CPU-Verbrauch über CFS-Quotas.
Sinnvolle Grenzwerte ermittelt man durch Profiling unter realistischer Last – nicht durch Schätzung. Anwendungsseitig müssen Node.js-Prozesse über --max-old-space-size und MySQL über innodb_buffer_pool_size explizit über ihre Speichergrenzen informiert werden, weil diese Prozesse das Docker-Limit nicht automatisch kennen. In Docker Compose v2 konfiguriert man alle Limits zentral unter deploy.resources und kann die Werte über Umgebungsvariablen pro Umgebung variieren.
Docker CPU und Memory Limits — Das Wichtigste auf einen Blick
Hartes Memory-Limit
--memory + --memory-swap auf denselben Wert setzen, um Swap vollständig zu deaktivieren. Limit = 1,5× gemessener Peak unter Last.
CPU-Throttling
--cpus setzt ein hartes CFS-Quota-Limit. Bei Contention zusätzlich --cpu-shares für relative Prioritätsvergabe nutzen.
OOM-Diagnose
Exit-Code 137 = OOM-Kill. Bestätigung via dmesg | grep oom. Ursache im Anwendungscode, nicht nur Limit erhöhen.
Compose-Konfiguration
deploy.resources.limits und reservations in Compose v2. Werte über Umgebungsvariablen pro Umgebung parametrisieren.