Docker Compose: Speicherlimits gegen OOM setzen
Setzen Sie in Docker Compose Speicher- und CPU-Limits, damit ein Container Ihren VPS nicht beendet. Erfahren Sie, wann exit 137 auftritt und wie viel Swap sinnvoll ist.
Welche Wirkung ein Docker-Compose-Speicherlimit hat
Ein Docker-Compose-Speicherlimit ist eine feste Obergrenze, die der Linux-Kernel für die cgroup (Control Group, eine Kernel-Funktion zur Ressourcenmessung für eine Gruppe von Prozessen) eines Containers setzt. Setzen Sie deploy.resources.limits.memory für einen Dienst, kann dieser Container niemals mehr als den angegebenen Wert verwenden. Bei einem Überschreitungsversuch beendet der Kernel einen Prozess im Container. Der Container wird dann normalerweise mit dem Exit-Code 137 beendet.
Das ist besonders auf einem VPS wichtig, weil der Arbeitsspeicher fest begrenzt ist und kein zusätzlicher Host-Speicher zur Verfügung steht. Ein Container mit einem Speicherleck oder einer fehlerhaften Abfrage belegt auf einem System mit 8GB den gesamten freien Arbeitsspeicher. Der Kernel beendet dann den Prozess, den er als am problematischsten bewertet. Das ist häufig eine Datenbank oder Ihre SSH-Sitzung und nicht der Container, der das Problem verursacht hat. Limits machen aus einem Ausfall des gesamten Servers einen einzelnen Dienst, der neu gestartet wird.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mWenden Sie die Einstellung an und prüfen Sie, ob das Limit aktiv ist:
docker compose up -d
docker stats --no-streamIn der Spalte MEM USAGE / LIMIT sollte ein Wert wie 142MiB / 1GiB stehen. Wenn die Spalte für das Limit den gesamten Arbeitsspeicher des Hosts anzeigt, wurde die Einstellung nicht übernommen. Die weiteren Schritte dieser Anleitung helfen erst, wenn das Limit aktiv ist. Wenn Docker Compose für Sie neu ist, beschreibt die Docker-Compose-Grundlagen für einen VPS den Dateiaufbau, auf dem diese Konfiguration aufbaut.
deploy.resources.limits oder mem_limit: Welche Einstellung gilt?
Für dieselbe Funktion gibt es zwei Schreibweisen. Das sorgt häufig für Verwirrung.
mem_limit, mem_reservation, memswap_limit, cpus und cpu_shares sind Service-Schlüssel auf oberster Ebene, die aus älteren Compose-Dateiformaten stammen. deploy.resources stammt aus dem Swarm-Schema und ist inzwischen Teil der Compose Specification, dem Format, das docker compose heute einliest.
Beide Varianten funktionieren auf einem einzelnen Host. Docker Compose, das docker compose-Plugin, wendet deploy.resources.limits und deploy.resources.reservations an, wenn Sie docker compose up ausführen. Dafür ist kein Swarm-Cluster erforderlich. Das Plugin befindet sich inzwischen in der v5-Reihe. Beim Test am 2026-10-10 war v5.6.0 die aktuelle stabile Version. Der Versionssprung erfolgte direkt von v2 auf v5. Die Versionen 3 und 4 wurden übersprungen, um Verwechslungen mit den alten Compose-Dateiformaten zu vermeiden. Keine v5-Version hat die Verarbeitung von Ressourcenlimits geändert. Die Swarm-orientierten Bestandteile des deploy-Blocks sind die anderen Schlüssel, zum Beispiel placement, update_config und endpoint_mode. Sie beschreiben die Planung und Rollouts über einen Cluster aus Nodes für docker stack deploy. Die verbreitete Aussage, dass „deploy Swarm benötigt“, ist für den Ressourcenabschnitt daher falsch. Wenn Sie dieser Aussage folgen, laufen Ihre Services vollständig ohne Limit.
Verwenden Sie pro Projekt nur eine Schreibweise. mem_limit: 512m und deploy.resources.limits.memory: 1g für denselben Service zu setzen, ist nicht nur schwer lesbar: Die Compose Specification verlangt, dass mem_limit konsistent mit limits.memory ist, wenn mem_limit gesetzt wird. Sie legt jedoch nicht fest, welcher Wert bei einem Widerspruch Vorrang hat. Unabhängig von der verwendeten Schreibweise sollten Sie prüfen, welche Werte der Daemon tatsächlich angewendet hat:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Speicherwerte werden in Bytes angegeben. Daher gibt 1g den Wert als 1073741824 aus. Die CPU-Angabe erfolgt in Nano-CPUs. Daher gibt 1.5 den Wert als 1500000000 aus. Ein 0 in einem Feld bedeutet, dass kein Limit gesetzt wurde. Das kleinste von Docker akzeptierte Speicherlimit ist 6m. Bei einem niedrigeren Wert wird der Container nicht gestartet.
Was passiert, wenn ein Container das Limit erreicht?
Der Container wird nicht langsamer. Er wird beendet.
Wenn ein Prozess eine Speicherseite anfordert und die cgroup bereits ihr memory.max erreicht hat, gibt der Kernel zunächst innerhalb dieser cgroup frei, was möglich ist: zuerst saubere Seiten aus dem Page Cache, anschließend Seiten, die ausgelagert werden können. Wenn dadurch nicht genügend Speicher freigegeben wird, wählt der cgroup-OOM-Killer einen Prozess innerhalb des Containers aus und sendet ihm SIGKILL. Wenn PID 1 des Containers beendet wird, endet der Container. Der Exit-Code 137 ist einfach 128 plus Signal 9. 137 ist daher das Kennzeichen für ein beliebiges SIGKILL, aber kein Beweis für einen OOM-Kill.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 ist ein OOM-Kill. false 137 bedeutet normalerweise, dass etwas anderes SIGKILL gesendet hat. Ein Programm kann zwar auch absichtlich mit 137 beendet werden, das ist aber selten. Die übliche Ursache ist, dass docker compose stop seine Kulanzfrist von zehn Sekunden erreicht, weil die Anwendung SIGTERM ignoriert hat. Diese Unterscheidung spart Stunden, weil die beiden Probleme nichts miteinander zu tun haben.
Zwei weitere Stellen protokollieren das Ereignis. Überwachen Sie den Daemon live:
docker events --filter event=oomLesen Sie anschließend das Kernel-Log. Dieses Protokoll bleibt auch nach einem Neustart erhalten:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Ein cgroup-Kill erzeugt eine Zeile, die mit Memory cgroup out of memory: Killed process 24713 (node) beginnt. Diese Zeile bedeutet, dass ein Container sein eigenes Limit erreicht hat. Sie kann erscheinen, obwohl auf dem Host noch viel freier RAM vorhanden ist. Eine Zeile ohne das Präfix Memory cgroup weist auf einen Host-OOM hin. Das bedeutet, dass dem Rechner selbst der RAM ausgegangen ist. Genau das sollen die Limits verhindern. Wenn diese Meldung erscheint, ist meist die Summe Ihrer Limits zu hoch, einige Dienste haben überhaupt kein Limit oder etwas außerhalb von Docker verwendet mehr Speicher als die eingeplante Reserve.
Mit restart: unless-stopped bleibt eine OOM-Schleife leicht unbemerkt, weil der Dienst eine Sekunde nach seinem Absturz in docker compose ps wieder als aktiv erscheint. Prüfen Sie die Uptime-Spalte und den Neustartzähler. Kombinieren Sie das Limit außerdem mit einem Healthcheck, der die Anwendung als fehlerhaft meldet, damit ein ständig beendeter Container sichtbar bleibt, ohne dass Sie ihn laufend überwachen müssen.
Reservation ist ein Hinweis, das Limit ist die Regel
reservations.memory (das ältere mem_reservation) ist eine weiche Untergrenze. Docker beschreibt sie als Soft Limit, das aktiviert wird, wenn der Daemon eine hohe Speicherauslastung oder zu wenig Arbeitsspeicher auf dem Host erkennt. Sie verhindert nicht, dass ein Container darüber hinaus Speicher verwendet, und garantiert nicht, dass beim Speicherbedarf des Containers freier Speicher verfügbar ist. Sie veranlasst den Kernel lediglich, zuerst Speicher von Containern zurückzufordern, die über ihrer Reservation liegen.
Eine Reservation schützt daher allein nichts. Verwenden Sie sie, um einen Dienst zu kennzeichnen, der unter hoher Last bevorzugt behandelt werden soll. Verlassen Sie sich für die Begrenzung auf das Limit. Halten Sie die Reservation unterhalb des Limits. Andernfalls startet der Container nicht: Docker lehnt die Konfiguration mit Minimum memory limit can not be less than memory reservation limit ab.
Swap-Accounting, realistisch betrachtet
Die meisten VPS-Images werden ohne Swap-Datei ausgeliefert. Führen Sie swapon --show und free -h aus. Wenn der gesamte Swap-Wert null ist, haben alle folgenden Swap-Einstellungen keine Wirkung, und Ihr Speicherlimit begrenzt ausschließlich den RAM.
memswap_limit bezeichnet nicht die Swap-Größe. Der Wert umfasst den gesamten Speicher aus RAM und Swap. Mit mem_limit: 1g und memswap_limit: 2g erhält der Container 1GB RAM und 1GB Swap. Wenn Sie beide Werte gleich setzen, hat der Container überhaupt keinen Swap. Wenn Sie mem_limit setzen und memswap_limit nicht festlegen, kann der Container wieder bis zur Größe seines Speicherlimits auslagern.
Ubuntu 24.04 und Debian 13 verwenden standardmäßig cgroup v2. Dort wird Swap separat gezählt (memory.swap.max), und dies funktioniert ohne zusätzliche Einrichtung. Die alte Meldung Your kernel does not support swap limit capabilities stammt von cgroup-v1-Hosts, die ohne swapaccount=1 gestartet wurden. Auf diesen Hosts gilt das Speicherlimit weiterhin, während der Swap-Anteil ignoriert wird.
Seien Sie realistisch, welchen Nutzen Swap bietet. Ein OOM-Kill wird dadurch langsamer, aber nicht unwahrscheinlicher, weil ein Prozess mit Speicherleck Swap genauso bereitwillig füllt wie RAM. Gleichzeitig verlangsamt ein Container, der auf gemeinsam genutztem VPS-Speicher ständig auslagert, alle anderen Dienste auf dem Host. Für Anwendungen, bei denen die Latenz wichtig ist, schlägt ein korrekt gesetztes Limit ohne Swap schneller und vorhersehbarer fehl.
Warum die Speichernutzung schlechter aussieht, als sie ist
Unter Linux zieht die MEM USAGE-Anzeige in docker stats nur den inaktiven Page Cache ab (inactive_file unter cgroup v2). Kürzlich verwendeter Cache wird daher weiterhin mitgezählt. Ein Container, der große Dateien liest, steigt bis nahe an sein Limit und bleibt dort. Das ist normal und kein Speicherleck, weil sauberer Cache freigegeben wird, bevor der OOM-Killer überhaupt aufgerufen wird. Ein Dienst wie ein selbst gehosteter Jellyfin-Medienserver bleibt aus genau diesem Grund scheinbar dauerhaft nahe an seiner Obergrenze.
Teilen Sie den Wert innerhalb des Containers in Cache und tatsächlich benötigte Arbeitsspeichermenge auf:
docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.eventsanon ist anonymer Speicher, also der nicht freigebbare Arbeitsspeicher. file ist Page Cache, der freigegeben werden kann. Legen Sie das Limit anhand von anon plus einem Puffer fest, nicht anhand der Gesamtsumme. Die Datei memory.events liefert die eindeutige Antwort: Ein oom_kill-Zähler über null bedeutet, dass der Kernel seit dem Start des Containers etwas darin beendet hat. Ein steigender max-Zähler bedeutet, dass der Container gerade an seiner Obergrenze gehalten wird. Beide Befehle benötigen eine Shell und coreutils im Image. Daher schlagen sie bei einem distroless- oder scratch-Image fehl.
Sizing-Grenzen auf einem 8GB-VPS
Beginnen Sie beim Host, nicht bei den Anwendungen. Lassen Sie auf einem 8GB-VPS etwa 1GB für den Kernel, den Docker-Daemon, sshd, journald und Ihre eigene Login-Shell frei. Damit bleiben ungefähr 7GB zur Verteilung. Die Summe aller Container-Limits sollte darunter bleiben. Overcommit funktioniert genau bis zu dem Tag, an dem zwei Dienste gleichzeitig ihren Spitzenverbrauch erreichen.
Eine praktikable Aufteilung auf einem 8GB-System:
- Reverse Proxy: Limit von 128m. Der Prozess ist klein. Ein derart knappes Limit erkennt eine außer Kontrolle geratene Konfigurationsneuladung sofort.
- PostgreSQL: Limit von 2g, mit
shared_buffersauf etwa 512MB in der Datenbankkonfiguration. - Anwendungscontainer: Limit von 1g.
- Hintergrund-Worker: Limit von 512m.
- Medien- oder Dateidienst: Limit von 2g. Der größte Teil davon wird für den Page Cache verwendet.
Übernehmen Sie diese Zahlen nicht unverändert für Ihren eigenen Stack. Betreiben Sie die Dienste einen Tag lang unter realer Last. Überwachen Sie docker stats, ermitteln Sie für jeden Container den Spitzenwert anon und rechnen Sie ungefähr die Hälfte zusätzlich als Reserve ein. Ein zu knapp gesetztes Limit ist schlechter als gar kein Limit. Es beendet einen gesunden Dienst während eines normalen Lastanstiegs.
Eine Besonderheit verdient einen eigenen Hinweis. Einige Laufzeitumgebungen ignorieren das Limit, sofern Sie sie nicht darüber informieren. Laufzeitumgebungen, die das Limit auslesen, dimensionieren sich möglicherweise nicht so, wie Sie erwarten. PostgreSQL dimensioniert shared_buffers und work_mem problemlos über das Container-Limit hinaus und wird dann beendet. Eine moderne JVM (Java virtual machine) erkennt das cgroup-Limit standardmäßig. HotSpot-Builds wie OpenJDK weisen dem Heap dann jedoch nur 25 Prozent davon zu. Mit -XX:MaxRAMPercentage=75 kann der Heap üblicherweise einen größeren Anteil des Limits verwenden, während Speicher außerhalb des Heaps verfügbar bleibt. Node.js dimensioniert seinen Standard-Heap seit Version 12 anhand des cgroup-Limits. Der Wert beträgt ungefähr die Hälfte des Container-Limits und flacht bei etwa 2GB ab. Setzen Sie --max-old-space-size daher nur dann in Megabytes, wenn Sie eine andere Obergrenze benötigen. Der Wert sollte unter dem Container-Limit bleiben, damit der Garbage Collector vor dem Eingreifen des Kernels arbeiten kann. Bei Ollama gilt dasselbe mit einem anderen Parameter, weil eine Erhöhung von num_ctx den KV-Cache vergrößert — um Hunderte Megabytes — und der Container während eines langen Prompts beendet wird. Das cgroup-Limit verhandelt nicht. Es beendet den Prozess.
CPU-Limits funktionieren grundlegend anders
cpus: "1.5" bedeutet 150 % eines CPU-Kerns. Der Wert wird als CFS-Quota (Completely Fair Scheduler) erzwungen. Der Container erhält in jedem Zeitraum von 100 ms insgesamt 150 ms CPU-Zeit, die auf alle seine Threads verteilt wird. Sobald diese Zeit aufgebraucht ist, lässt der Kernel den Container bis zum nächsten Zeitraum warten.
Das ist der entscheidende Unterschied. Ein Container, der sein Speicherlimit überschreitet, wird beendet. Ein Container, der sein CPU-Limit überschreitet, wird gedrosselt und läuft mit geringerer Geschwindigkeit weiter. Daher können Sie ein CPU-Limit großzügig festlegen. Bei einem Speicherlimit benötigen Sie dagegen Reserve.
cpu_shares ist ein anderes Werkzeug. Dabei handelt es sich um eine relative Gewichtung, die nur greift, wenn die CPUs tatsächlich ausgelastet sind. Zwei Container mit Shares von 1024 und 512 teilen sich einen ausgelasteten CPU-Kern ungefähr im Verhältnis zwei zu eins. Auf einem nicht ausgelasteten System wird keiner der beiden Container eingeschränkt. Verwenden Sie Shares, um Dienste nach ihrer Wichtigkeit zu priorisieren. Verwenden Sie cpus, wenn Sie eine echte Obergrenze benötigen, beispielsweise um zu verhindern, dass ein nächtlicher Transcodierungsjob Ihren Webserver ausbremst.
FAQ
Funktioniert deploy.resources.limits ohne Docker Swarm?
Ja. Docker Compose, das docker compose-Plugin (inzwischen in der v5-Linie), wendet deploy.resources.limits und deploy.resources.reservations an, wenn Sie docker compose up auf einem einzelnen Host ausführen. Überprüfen Sie dies mit docker inspect --format '{{.HostConfig.Memory}}' <container>. Der Befehl gibt das Limit in Bytes aus und gibt 0 aus, wenn kein Limit angewendet wurde. Die Schlüssel innerhalb von deploy, die mit Blick auf Swarm geschrieben wurden, sind die Cluster-Schlüssel, beispielsweise placement, update_config und endpoint_mode.
Was bedeutet der Exit-Code 137 in Docker Compose?
Fast immer bedeutet er, dass der Hauptprozess SIGKILL empfangen hat, da 137 aus 128 plus Signal 9 besteht. Ein Programm kann auch absichtlich mit 137 beendet werden, das ist jedoch selten. Der OOM-Killer des Kernels ist die häufigste Ursache. Ein Timeout beim Herunterfahren erzeugt denselben Code, wenn eine Anwendung SIGTERM ignoriert. Führen Sie docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> aus, um die beiden Ursachen zu unterscheiden. true 137 steht für einen Speicherkill, false 137 nicht.
Sollte ich mem_limit oder deploy.resources.limits.memory setzen?
Beides funktioniert mit docker compose. deploy.resources.limits.memory ist die aktuelle Form der Compose Specification und für eine neue Datei die bessere Standardeinstellung. Behalten Sie mem_limit bei, wenn Ihre Datei bereits die älteren Schlüssel auf oberster Ebene verwendet. Die Compose Specification verlangt, dass beide Werte konsistent sind, wenn beide gesetzt wurden. Wählen Sie daher eine Variante und überprüfen Sie das Ergebnis mit docker inspect.
Warum erreicht mein Container sein vollständiges Speicherlimit, ohne beendet zu werden?
Der Nutzungswert in docker stats zählt weiterhin kürzlich verwendeten Page Cache. Der Kernel verwirft diesen unter Speicherdruck, statt einen OOM-Kill auszulösen. Führen Sie docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat aus und lesen Sie den Wert anon ab. Er bezeichnet den Working Set, der nicht zurückgewonnen werden kann. Ein hoher Wert file neben einem niedrigen Wert anon weist auf einen Container mit Festplatten-Ein- und -Ausgabe hin, nicht auf einen Container, der kurz vor dem Beenden steht.
Wie viel RAM sollte ich auf einem 8GB-VPS nicht zuweisen?
Lassen Sie etwa 1GB für den Kernel, den Docker-Daemon, sshd, journald und Ihre eigene Shell frei. Halten Sie anschließend die Summe aller Container-Limits unter den verbleibenden 7GB. Überwachen Sie den Spitzenwert anon jedes Containers einen Tag lang unter realer Last, bevor Sie sich auf Werte festlegen. Betrachten Sie die Gesamtsumme als Budget und nicht als Zielwert, den Sie vollständig auslasten.