Docker CPU und Memory Limits sinnvoll setzen
AI generated
Docker · Ressourcen · cgroups · DevOps
Docker CPU und Memory Limits sinnvoll setzen
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.

12 Min. Lesezeit --cpus · --memory · --memory-swap · cgroups · OOM-Diagnose Docker 24+ · Compose v2 · Linux cgroups v2

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.

11. FAQ: Docker CPU und Memory Limits

1Was passiert beim Überschreiten des Memory-Limits?
Der OOM-Killer beendet Prozesse im Container. Exit-Code 137 und dmesg-Einträge sind die sicheren Diagnosezeichen.
2--memory vs. --memory-swap?
--memory begrenzt RAM, --memory-swap begrenzt RAM + Swap kombiniert. Gleicher Wert = kein Swap erlaubt.
3Wie sinnvolle Limits ermitteln?
Peak unter realistischer Last messen (docker stats), dann 1,5× als Limit setzen – nicht schätzen.
4Node.js und Docker Memory Limits?
--max-old-space-size immer explizit setzen. V8 kennt das Docker-Limit nicht automatisch und wächst ohne diesen Parameter darüber hinaus.
5--cpus vs. --cpu-shares?
--cpus = hartes Throttling via CFS-Quota. --cpu-shares = relative Gewichtung, wirkt nur bei Contention.
6Ressourcenlimits in Docker Compose?
deploy.resources.limits für harte Limits, deploy.resources.reservations für Soft-Hints. Compose v2, kein Swarm nötig.
7Was ist cgroups v2?
Einheitliche Linux-Ressourcenverwaltung. Docker 24+ unterstützt vollständig. Prüfen: stat -fc %T /sys/fs/cgroup/.
8Dauerhaftes Monitoring von Limits?
cAdvisor + Prometheus + Grafana für produktive Setups. Einfach: Cron-Job mit docker stats --no-stream.
9Langsamer Container ohne OOM-Kill?
CPU-Throttling durch --cpus oder Swap-Thrashing durch knappes Memory-Limit. container_cpu_cfs_throttled_seconds_total in Prometheus zeigt Throttling.
10Limits auch in der Entwicklung setzen?
Empfehlenswert. Verhindert Abstürze des Entwicklungsrechners und deckt Ressourcenprobleme früh auf, bevor sie in Produktion auftreten.