Docker Compose: Speicherlimits gegen OOM setzen
Setzen Sie in Docker Compose Speicher- und CPU-Limits, damit ein Container Ihren VPS nicht lahmlegt. Erkennen Sie Exit-Code 137 und planen Sie Swap korrekt.
Was ein Docker-Compose-Speicherlimit bewirkt
Ein Docker-Compose-Speicherlimit ist eine harte Obergrenze, die der Linux-Kernel für die cgroup eines Containers festlegt. Eine cgroup (Control Group) ist eine Kernel-Funktion, die Ressourcen für eine Gruppe von Prozessen misst. Setzen Sie deploy.resources.limits.memory für einen Service, kann dieser Container nie mehr als den angegebenen Wert verwenden. Bei einem Überschreitungsversuch beendet der Kernel einen Prozess im Container. Der Container wird dann normalerweise mit Code 137 beendet.
Das ist besonders auf einem VPS wichtig. Dort ist der Arbeitsspeicher fest begrenzt, und es gibt keinen zusätzlichen Host-Speicher. Ein Container mit einem Speicherleck oder einer fehlerhaften Abfrage kann jede freie Speicherseite auf einem System mit 8GB belegen. Der Kernel beendet dann den Prozess, den er als problematischsten einstuft. Das ist häufig eine Datenbank oder Ihre SSH-Sitzung und nicht der Container, der das Problem verursacht hat. Limits begrenzen einen Ausfall des gesamten Servers auf einen einzelnen Service, 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 bestätigen Sie, dass 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 Limitspalte den gesamten Arbeitsspeicher des Hosts anzeigt, wurde die Einstellung nicht angewendet. Die restlichen Schritte dieser Anleitung helfen erst, wenn das Limit aktiv ist. Wenn Ihnen die Compose-Datei noch nicht vertraut ist, erklärt die Docker-Compose-Grundlagen für einen VPS den zugrunde liegenden Dateiaufbau.
deploy.resources.limits oder mem_limit: Welche Einstellung gilt?
Für dieselbe Funktion gibt es zwei Schreibweisen. Das führt häufig zu Verwirrung.
mem_limit, mem_reservation, memswap_limit, cpus und cpu_shares sind Service-Schlüssel der obersten Ebene, die aus älteren Compose-Dateiformaten übernommen wurden. deploy.resources stammt aus dem Swarm-Schema und ist heute Teil der Compose Specification. Dieses Format liest docker compose heute.
Beide Varianten funktionieren auf einem einzelnen Host. Compose V2, das docker compose-Plugin, wendet deploy.resources.limits und deploy.resources.reservations an, wenn Sie docker compose up ausführen, auch wenn kein Swarm-Cluster vorhanden ist. Die nur für Swarm relevanten Teile des Blocks deploy sind die anderen Schlüssel: mode, placement, update_config und endpoint_mode haben für docker stack deploy eine Bedeutung und werden von docker compose up ignoriert. Die verbreitete Aussage, dass "deploy Swarm benötigt", ist für den Abschnitt resources daher falsch. Wenn Sie dieser Aussage folgen, laufen Ihre Services vollständig ohne Limit.
Verwenden Sie pro Projekt nur eine Schreibweise. Wenn Sie mem_limit: 512m und deploy.resources.limits.memory: 1g für denselben Service festlegen, ist auf Anhieb nicht erkennbar, welcher Wert gilt. Fragen Sie statt zu raten den Daemon:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Speicherwerte werden in Bytes angegeben. Daher gibt 1g den Wert 1073741824 aus. CPU-Werte werden in Nano-CPUs angegeben. Daher gibt 1.5 den Wert 1500000000 aus. Ein 0 in einem Feld bedeutet, dass kein Limit festgelegt wurde. Das kleinste von Docker akzeptierte Speicherlimit ist 6m. Unterhalb dieses Werts verweigert der Container den Start.
Was passiert, wenn ein Container das Limit erreicht
Der Container wird nicht langsamer. Er beendet sich.
Wenn ein Prozess eine Speicherseite anfordert und die cgroup bereits ihr memory.max erreicht hat, gibt der Kernel zuerst Speicher innerhalb dieser cgroup frei: sauberen Page Cache und anschließend Seiten, die ausgelagert werden können. Reicht das Freigeben nicht aus, wählt der OOM-Killer (Out of Memory) der cgroup einen Prozess innerhalb des Containers aus und sendet ihm SIGKILL. Wird die PID 1 des Containers beendet, endet der Container. Der Exit-Code 137 ist einfach 128 plus Signal 9. Daher ist 137 das Kennzeichen eines beliebigen SIGKILL, aber allein kein Beweis für einen OOM.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 ist ein OOM-Kill. false 137 bedeutet, dass etwas anderes SIGKILL gesendet hat. Die übliche Ursache ist, dass docker compose stop die 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. Es bleibt auch nach einem Neustart erhalten:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Ein Kill durch eine cgroup gibt eine Zeile aus, die mit Memory cgroup out of memory: Killed process 24713 (node) beginnt. Eine Zeile ohne das Präfix Memory cgroup ist ein OOM des Hosts. Das bedeutet, dass der Rechner selbst keinen freien RAM mehr hatte. Genau diesen Fehler sollen Limits verhindern. Sein Auftreten zeigt daher, dass die Summe Ihrer Limits zu hoch ist oder dass einige Dienste überhaupt kein Limit haben.
Bei restart: unless-stopped bleibt eine OOM-Schleife leicht unbemerkt, weil der Dienst eine Sekunde nach seinem Ende in docker compose ps wieder als aktiv erscheint. Prüfen Sie die Spalte für die Laufzeit und den Neustartzähler. Kombinieren Sie das Limit außerdem mit einem Healthcheck, der die Anwendung als fehlerhaft meldet, damit ein Container, der ständig beendet wird, sichtbar bleibt, ohne dass Sie ihn überwachen müssen.
Die Reservierung ist nur ein Hinweis, das Limit ist verbindlich
reservations.memory (das ältere mem_reservation) ist eine weiche Untergrenze. Docker beschreibt sie als weiches Limit, das aktiviert wird, wenn der Daemon auf dem Host eine Ressourcenknappheit oder wenig Arbeitsspeicher erkennt. Sie verhindert nie, dass ein Container darüber hinausgeht, und garantiert nie, dass der Arbeitsspeicher verfügbar ist, wenn der Container ihn anfordert. Sie veranlasst den Kernel lediglich dazu, zuerst Arbeitsspeicher von Containern zurückzufordern, die über ihrer Reservierung liegen.
Eine Reservierung schützt daher allein nichts. Verwenden Sie sie, um einen Dienst zu kennzeichnen, der bei Ressourcenknappheit bevorzugt behandelt werden soll, und verlassen Sie sich für die Sicherheit auf das Limit. Halten Sie die Reservierung unter dem Limit, sonst startet der Container nicht: Docker lehnt die Konfiguration mit Minimum memory limit can not be less than memory reservation limit ab.
Swap-Verwaltung, korrekt erklärt
Die meisten VPS-Images werden ohne Swap-Datei ausgeliefert. Führen Sie swapon --show und free -h aus. Wenn die gesamte Swap-Menge null ist, bewirkt keine der folgenden Swap-Einstellungen etwas, und Ihr Speicherlimit ist eine reine RAM-Begrenzung.
memswap_limit ist nicht die Swap-Menge. Es ist die Summe aus Speicher 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 definieren, kann der Container wieder bis zur Größe seines Speicherlimits in den Swap auslagern.
Ubuntu 24.04 und Debian 13 verwenden standardmäßig cgroup v2. Dort ist Swap ein separater Zähler (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 bei den Vorteilen von Swap realistisch. Swap macht einen OOM-Kill langsamer, aber nicht unwahrscheinlicher, weil ein Prozess mit Speicherleck den Swap genauso problemlos füllt wie den RAM. Währenddessen verlangsamt ein Container, der den Swap auf gemeinsam genutztem VPS-Speicher stark beansprucht, alle anderen Dienste auf dem Host. Für Anwendungen, bei denen die Latenz wichtig ist, führt ein korrektes Limit ohne Swap schneller und vorhersehbarer zum Fehlschlag.
Warum der Speicherverbrauch schlechter aussieht, als er ist
Die MEM USAGE-Zahl in docker stats enthält den Seiten-Cache. Deshalb steigt ein Container, der große Dateien liest, bis nahe an sein Limit und bleibt dort. Das ist normal und kein Leak, weil bereinigter Cache freigegeben wird, bevor der OOM-Killer überhaupt aufgerufen wird. Ein Dienst wie ein selbst gehosteter Jellyfin-Medienserver wirkt aus genau diesem Grund dauerhaft nahe an seiner Obergrenze.
Teilen Sie die Zahl 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 die Arbeitsspeichermenge, die nicht freigegeben werden kann. file ist Seiten-Cache und kann freigegeben werden. Legen Sie Ihr Limit anhand von anon plus einem Puffer fest, nicht anhand der Gesamtsumme. Die Datei memory.events beendet die Diskussion eindeutig: Ein oom_kill-Zähler größer als null bedeutet, dass der Kernel seit dem Start des Containers etwas 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.
Größenlimits 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 Containerlimits sollte darunter bleiben. Eine Überbelegung funktioniert genau bis zu dem Tag, an dem zwei Dienste gleichzeitig ihre Spitzenlast erreichen.
Eine praktikable Aufteilung auf einem 8GB-System:
- Reverse-Proxy: Limit von 128m. Er ist ein kleiner Prozess. Ein so knappes Limit erkennt eine endlose Konfigurationsneuladung sofort.
- PostgreSQL: Limit von 2g, wobei
shared_buffersin der Datenbankkonfiguration auf etwa 512MB gesetzt wird. - Anwendungscontainer: Limit von 1g.
- Hintergrund-Worker: Limit von 512m.
- Medien- oder Dateidienst: Limit von 2g. Der größte Teil davon wird Page Cache sein.
Ü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 addieren Sie ungefähr die Hälfte dieses Werts als Reserve. Ein zu knapp gesetztes Limit ist schlechter als gar kein Limit, weil es einen gesunden Dienst während eines normalen Lastanstiegs beendet.
Eine Falle verdient einen eigenen Hinweis. Für die meisten Laufzeitumgebungen ist das Limit unsichtbar, sofern Sie es ihnen nicht mitteilen. PostgreSQL dimensioniert shared_buffers und work_mem problemlos über das Containerlimit hinaus und wird dann beendet. Eine JVM (Java virtual machine) benötigt -XX:MaxRAMPercentage=75, damit sie ihren Heap anhand des cgroup-Limits statt anhand des Host-RAMs dimensioniert. Node.js benötigt --max-old-space-size in Megabytes, und dieser Wert muss unter dem Containerlimit liegen. Andernfalls lässt der Garbage Collector den Heap wachsen, bis der Kernel eingreift. Die cgroup verhandelt nicht. Sie beendet den Prozess.
CPU-Limits verhalten sich völlig anders
cpus: "1.5" bedeutet 150% eines Kerns, erzwungen als CFS-Kontingent (Completely Fair Scheduler). Der Container erhält in jedem Zeitraum von 100ms 150ms CPU-Zeit. Diese Zeit wird von allen Threads des Containers gemeinsam genutzt. Wenn sie 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. Deshalb können Sie ein CPU-Limit großzügig festlegen. Ein Speicherlimit benötigt dagegen Reserven.
cpu_shares ist ein anderes Werkzeug: eine relative Gewichtung, die nur relevant ist, wenn die CPUs tatsächlich ausgelastet sind. Zwei Container mit Shares von 1024 und 512 teilen sich einen ausgelasteten Kern ungefähr im Verhältnis zwei zu eins. Auf einem nicht ausgelasteten System wird keiner von beiden eingeschränkt. Verwenden Sie Shares, um Dienste nach ihrer Wichtigkeit zu priorisieren. Verwenden Sie cpus, wenn Sie eine tatsächliche Obergrenze benötigen, zum Beispiel um zu verhindern, dass ein nächtlicher Transkodierungsjob Ihren Webserver ausbremst.
FAQ
Funktioniert deploy.resources.limits ohne Docker Swarm?
Ja. Compose V2 wendet deploy.resources.limits und deploy.resources.reservations an, wenn Sie docker compose up auf einem einzelnen Host ausführen. Prüfen Sie dies mit docker inspect --format '{{.HostConfig.Memory}}' <container>. Der Befehl gibt das Limit in Byte aus und 0, wenn kein Limit angewendet wurde. Die Schlüssel innerhalb von deploy, die tatsächlich Swarm erfordern, sind mode, placement, update_config und endpoint_mode.
Was bedeutet der Exit-Code 137 in Docker Compose?
Der Hauptprozess hat SIGKILL empfangen, da 137 gleich 128 plus Signal 9 ist. Der OOM-Killer des Kernels ist die häufigste Ursache. Derselbe Code entsteht jedoch auch bei einer Zeitüberschreitung beim Beenden, wenn eine Anwendung SIGTERM ignoriert. Führen Sie docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> aus, um die Ursachen zu unterscheiden. true 137 steht für eine Speicherbeendigung, false 137 dagegen nicht.
Sollte ich mem_limit oder deploy.resources.limits.memory festlegen?
Beides funktioniert mit docker compose. deploy.resources.limits.memory ist die aktuelle Form der Compose Specification und die bessere Standardeinstellung für eine neue Datei. Behalten Sie mem_limit bei, wenn der übrige Inhalt Ihrer Datei bereits die älteren Schlüssel der obersten Ebene verwendet. Beide Angaben für denselben Service machen die Datei nur schwerer lesbar. Wählen Sie daher eine davon und prüfen Sie das Ergebnis mit docker inspect.
Warum erreicht mein Container sein vollständiges Speicherlimit, ohne beendet zu werden?
Der in docker stats angezeigte Nutzungswert umfasst den 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 Arbeitssatz, der nicht zurückgewonnen werden kann. Ein hoher Wert file neben einem niedrigen Wert anon weist auf einen Container mit Festplatten-Ein- und -Ausgaben hin, nicht auf einen Container, der unmittelbar beendet wird.
Wie viel RAM sollte ich auf einer 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 Containerlimits unter den verbleibenden 7GB. Überwachen Sie unter realer Auslastung einen Tag lang den Spitzenwert anon jedes Containers, bevor Sie sich auf Werte festlegen. Betrachten Sie die Gesamtsumme als Budget und nicht als Zielwert, den Sie vollständig ausschöpfen.