Docker Compose geheugenlimiet instellen tegen OOM
Voorkom dat een container uw VPS platlegt door geheugenlimieten in Docker Compose te configureren. Gebruik deploy.resources om exit 137 fouten te beheren en RAM-gebruik te begrenzen.
Wat een Docker Compose geheugenlimiet doet
Een Docker Compose geheugenlimiet is een harde bovengrens die de Linux-kernel oplegt aan de cgroup (control group, de kernel-functie die resources voor een set processen meet) van een container. Stel deploy.resources.limits.memory in op een service en die container kan nooit meer gebruiken dan het opgegeven aantal. Wanneer de container dit toch probeert, beëindigt de kernel een proces binnen de container en stopt de container doorgaans met exitcode 137.
Dit is vooral van belang op een VPS, waar het RAM-geheugen vaststaat en er geen extra host-geheugen beschikbaar is om te lenen. Eén container met een geheugenlek of een foutieve query kan elke vrije pagina op een 8GB-server verbruiken. De kernel beëindigt vervolgens het proces dat hij als meest problematisch beoordeelt; dit is vaak een database of uw SSH-sessie in plaats van de container die het probleem veroorzaakte. Limieten veranderen een serverbrede uitval in een herstart van slechts één service.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mPas het toe en controleer of de limiet actief is:
docker compose up -d
docker stats --no-streamDe kolom MEM USAGE / LIMIT zou iets als 142MiB / 1GiB moeten weergeven. Als de limietkolom het volledige RAM-geheugen van de host toont, is de instelling niet toegepast en zal de rest van deze handleiding niet helpen totdat dit wel het geval is. Als het compose-bestand zelf nieuw voor u is, behandelt de Docker Compose basis voor een VPS de bestandsstructuur waarop dit voortbouwt.
deploy.resources.limits of mem_limit: welke is van toepassing
Er bestaan twee schrijfwijzen voor hetzelfde concept, wat voor verwarring zorgt.
mem_limit, mem_reservation, memswap_limit, cpus en cpu_shares zijn service-keys op het hoogste niveau die zijn overgenomen uit de oudere Compose-bestandsformaten. deploy.resources is afkomstig uit het Swarm-schema en maakt nu deel uit van de Compose Specification, het formaat dat docker compose tegenwoordig leest.
Beide werken op een enkele host. Compose V2, de docker compose-plugin, past deploy.resources.limits en deploy.resources.reservations toe wanneer u docker compose up uitvoert, zonder dat er een Swarm-cluster aanwezig is. De onderdelen van het deploy-blok die specifiek voor Swarm zijn, zijn de overige keys: mode, placement, update_config en endpoint_mode betekenen iets voor docker stack deploy en worden genegeerd door docker compose up. Het algemene advies dat "deploy Swarm vereist" is dus onjuist voor de resources-subsectie; wie dit advies opvolgt, laat services achter zonder enige limiet.
Kies één schrijfwijze per project. Het gebruik van zowel mem_limit: 512m als deploy.resources.limits.memory: 1g op dezelfde service resulteert in een bestand dat voor niemand in één oogopslag leesbaar is. In plaats van te raden welk getal de overhand krijgt, kunt u dit aan de daemon vragen:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Geheugenwaarden worden uitgedrukt in bytes, dus 1g wordt weergegeven als 1073741824. CPU wordt uitgedrukt in nano-CPU's, dus 1.5 wordt weergegeven als 1500000000. Een 0 in een veld betekent dat er geen limiet is ingesteld. De kleinste geheugenlimiet die Docker accepteert is 6m; daaronder weigert de container op te starten.
Wat gebeurt er wanneer een container de limiet bereikt
De container vertraagt niet. Hij stopt.
Wanneer een proces om een pagina vraagt en de cgroup bevindt zich al op zijn memory.max, dan claimt de kernel eerst terug wat mogelijk is binnen die cgroup: schone page cache, gevolgd door pagina's die naar swap kunnen worden verplaatst. Als dit onvoldoende geheugen vrijmaakt, kiest de cgroup OOM (out of memory) killer een proces binnen de container en stuurt dit een SIGKILL. Het beëindigen van PID 1 van de container stopt de container. Exitcode 137 is simpelweg 128 plus signaal 9; 137 is dus de vingerafdruk van elke SIGKILL en op zichzelf geen bewijs van een OOM.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 duidt op een OOM-kill. false 137 betekent dat iets anders een SIGKILL heeft verstuurd. De gebruikelijke oorzaak hiervan is dat docker compose stop zijn respijtperiode van tien seconden heeft bereikt omdat de applicatie SIGTERM negeerde. Dit onderscheid bespaart uren werk, aangezien beide problemen niets met elkaar gemeen hebben.
Twee andere locaties registreren het evenement. Monitor de daemon live:
docker events --filter event=oomLees vervolgens het kernellogboek, het verslag dat een herstart overleeft:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Een cgroup-kill print een regel die begint met Memory cgroup out of memory: Killed process 24713 (node). Een regel zonder het Memory cgroup-voorvoegsel is een host-OOM, wat betekent dat de machine zelf onvoldoende RAM had. Dit is de fout die limieten juist moeten voorkomen; het zien hiervan is een teken dat de som van uw limieten te hoog is, of dat sommige services helemaal geen limiet hebben.
Bij restart: unless-stopped is een OOM-loop lastig te detecteren, omdat de service in docker compose ps weer actief lijkt een seconde nadat deze is gestopt. Controleer de uptime-kolom en het aantal herstarts, en combineer de limiet met een healthcheck die de applicatie als ongezond rapporteert, zodat een container die continu blijft sterven zichtbaar is zonder dat u dit handmatig hoeft te monitoren.
Een reservering is een indicatie, de limiet is de regel
reservations.memory (de oudere mem_reservation) is een zachte ondergrens. Docker beschrijft dit als een soft limit die wordt geactiveerd wanneer de daemon op de host geheugentekort of strijd om resources detecteert. Het voorkomt niet dat een container boven deze waarde uitkomt en garandeert niet dat er geheugen vrij is wanneer de container erom vraagt. Het zorgt er enkel voor dat de kernel bij geheugengebrek prioriteit geeft aan het vrijmaken van geheugen bij containers die boven hun reservering zitten.
Een reservering biedt op zichzelf dus geen bescherming. Gebruik deze om een service aan te duiden die bij hoge systeembelasting voorrang moet krijgen, en vertrouw op de limiet voor de veiligheid. Houd de reservering onder de limiet, anders zal de container niet starten: Docker weigert de configuratie met Minimum memory limit can not be less than memory reservation limit.
Swap-accounting, eerlijk bekeken
De meeste VPS-images worden geleverd zonder swap-bestand. Voer swapon --show en free -h uit. Als het totaal aan swap nul is, hebben alle onderstaande swap-instellingen geen effect en fungeert uw geheugenlimiet als een harde limiet op het RAM-geheugen.
memswap_limit is niet de hoeveelheid swap. Het is het totaal van geheugen plus swap. Met mem_limit: 1g en memswap_limit: 2g krijgt de container 1GB RAM en 1GB swap. Door beide waarden gelijk in te stellen, krijgt de container helemaal geen swap. Door mem_limit in te stellen en memswap_limit ongewijzigd te laten, kan de container opnieuw swap gebruiken tot de grootte van zijn geheugenlimiet.
Ubuntu 24.04 en Debian 13 gebruiken standaard cgroup v2, waarbij swap een afzonderlijke teller is (memory.swap.max) en dit werkt zonder extra configuratie. De oude melding Your kernel does not support swap limit capabilities is afkomstig van cgroup v1-hosts die zijn opgestart zonder swapaccount=1. Op die systemen blijft de geheugenlimiet van kracht, terwijl het swap-gedeelte wordt genegeerd.
Wees eerlijk over wat swap u oplevert. Het maakt een OOM-kill trager, niet minder waarschijnlijk, omdat een lekker proces swap net zo gemakkelijk vult als RAM. Ondertussen zorgt een container die swap op gedeelde VPS-opslag uitput voor vertraging van elke andere service op de server. Voor alles wat gevoelig is voor latentie, zorgt een correcte limiet zonder swap ervoor dat fouten sneller en voorspelbaarder optreden.
Waarom geheugengebruik er slechter uitziet dan het is
Het MEM USAGE-cijfer in docker stats bevat de page cache, waardoor een container die grote bestanden leest richting zijn limiet klimt en daar blijft staan. Dit is normaal en geen lek, omdat schone cache wordt vrijgegeven voordat de OOM killer ooit wordt aangeroepen. Een service zoals een zelfgehoste Jellyfin mediaserver zal om precies deze reden permanent dicht bij zijn plafond lijken te zitten.
Splits het getal van binnenuit de container in cache en de werkelijke werkset:
docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.eventsanon is anoniem geheugen, de werkset die niet kan worden verwijderd. file is page cache, die wel kan worden verwijderd. Bepaal uw limiet op basis van anon plus een marge, niet op basis van het totaal. Het memory.events-bestand beslecht de discussie direct: een oom_kill-teller boven nul betekent dat de kernel sinds de start iets in deze container heeft beëindigd, en een stijgende max-teller betekent dat de container op dit moment op zijn plafond wordt gehouden. Beide commando's vereisen een shell en coreutils in de image; ze falen daarom op een distroless of scratch image.
Groottebeperkingen op een 8GB VPS
Begin bij de host, niet bij de applicaties. Reserveer op een 8GB VPS ongeveer 1GB voor de kernel, de Docker daemon, sshd, journald en uw eigen login-shell. Dit laat ongeveer 7GB over om toe te wijzen; de som van alle containerlimieten moet hieronder blijven. Overcommitment werkt totdat twee services tegelijkertijd hun piek bereiken.
Een werkbare verdeling op een 8GB-systeem:
- Reverse proxy: 128m limiet. Dit is een klein proces; een krappe limiet detecteert een vastgelopen configuratie-reload direct.
- PostgreSQL: 2g limiet, met
shared_buffersingesteld op ongeveer 512MB in de databaseconfiguratie. - Applicatiecontainer: 1g limiet.
- Achtergrondworker: 512m limiet.
- Media- of bestandsservice: 2g limiet, waarvan het grootste deel uit page cache zal bestaan.
Kopieer deze getallen niet blindelings naar uw eigen stack. Laat de services een dag onder reële belasting draaien, monitor docker stats, noteer de piekwaarde van anon per container en voeg daar ongeveer de helft aan toe als reserve. Een te krappe limiet is slechter dan geen limiet, omdat deze een gezonde service beëindigt tijdens een normale piek in het verkeer.
Eén valkuil verdient extra aandacht. De limiet is onzichtbaar voor de meeste runtimes, tenzij u ze hierover informeert. PostgreSQL zal vrolijk shared_buffers en work_mem instellen boven de containerlimiet, waarna het proces wordt beëindigd. Een JVM (Java virtual machine) heeft -XX:MaxRAMPercentage=75 nodig om de heap te baseren op de cgroup-limiet in plaats van op het RAM-geheugen van de host. Node.js heeft --max-old-space-size nodig in megabytes, ingesteld onder de containerlimiet; anders laat de garbage collector de heap groeien totdat de kernel ingrijpt. Voor Ollama geldt hetzelfde met een andere instelling, omdat het verhogen van num_ctx de KV-cache vergroot met honderden megabytes, waardoor de container halverwege een lange prompt crasht. De cgroup onderhandelt niet. Deze beëindigt het proces.
CPU-limieten gedragen zich fundamenteel anders
cpus: "1.5" staat voor 150% van één core, afgedwongen als een CFS (completely fair scheduler) quota. De container krijgt 150ms aan CPU-tijd in elke periode van 100ms, verdeeld over alle threads. Wanneer dit verbruikt is, laat de kernel de container wachten tot de volgende periode.
Dit is het belangrijke contrast. Een container die zijn geheugenlimiet overschrijdt, wordt beëindigd. Een container die zijn CPU-limiet overschrijdt, wordt vertraagd en gaat door, maar langzamer. Een CPU-limiet kan daarom agressief worden ingesteld, terwijl een geheugenlimiet ruimte nodig heeft.
cpu_shares is een ander hulpmiddel: een relatief gewicht dat alleen relevant is wanneer de CPU's daadwerkelijk verzadigd zijn. Twee containers met shares van 1024 en 512 verdelen een bezette core in een verhouding van ongeveer twee op één, en op een systeem dat niet belast is, wordt geen van beide beperkt. Gebruik shares om services op prioriteit te rangschikken en gebruik cpus wanneer u een hard plafond nodig heeft, bijvoorbeeld om te voorkomen dat een nachtelijke transcode-taak uw webserver vertraagt.
FAQ
Werkt deploy.resources.limits zonder Docker Swarm?
Ja. Compose V2 past deploy.resources.limits en deploy.resources.reservations toe wanneer u docker compose up uitvoert op een enkele host. Controleer dit met docker inspect --format '{{.HostConfig.Memory}}' <container>, dat de limiet in bytes weergeeft en 0 toont wanneer er geen limiet is toegepast. De sleutels binnen deploy die daadwerkelijk Swarm vereisen, zijn mode, placement, update_config en endpoint_mode.
Wat betekent exitcode 137 in Docker Compose?
Dit betekent dat het hoofdproces een SIGKILL heeft ontvangen, aangezien 137 gelijk is aan 128 plus signaal 9. De OOM-killer van de kernel is de meest voorkomende oorzaak, maar een afsluit-timeout produceert dezelfde code wanneer een applicatie SIGTERM negeert. Voer docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> uit om het onderscheid te maken. true 137 duidt op een geheugen-kill, en false 137 niet.
Moet ik mem_limit of deploy.resources.limits.memory instellen?
Beide werken met docker compose. deploy.resources.limits.memory is de huidige vorm volgens de Compose Specification en is de betere standaard voor een nieuw bestand. Behoud mem_limit als de rest van uw bestand al gebruikmaakt van de oudere top-level sleutels. Het instellen van beide op één service maakt het bestand alleen maar onleesbaarder, dus kies er één en verifieer het resultaat met docker inspect.
Waarom zit mijn container op zijn volledige geheugenlimiet zonder te worden beëindigd?
Het verbruikscijfer in docker stats bevat de page cache, die de kernel onder druk vrijgeeft in plaats van een OOM-kill te triggeren. Voer docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat uit en lees de waarde anon, dit is de working set die niet kan worden teruggewonnen. Een hoge file-waarde naast een lage anon-waarde duidt op een container die schijf-input en -output verwerkt, niet op een container die op het punt staat te crashen.
Hoeveel RAM moet ik ongebruikt laten op een 8GB VPS?
Houd ongeveer 1GB vrij voor de kernel, de Docker daemon, sshd, journald en uw eigen shell, en houd de som van alle containerlimieten onder de resterende 7GB. Monitor de piekwaarde van anon per container onder werkelijke belasting gedurende een dag voordat u definitieve getallen vastlegt, en behandel het totaal als een budget in plaats van als een doel om volledig te benutten.