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 en crashes te beheersen.
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 probeert, beëindigt de kernel een proces in de container en stopt de container meestal 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 alle vrije pagina's op een 8GB-server verbruiken. De kernel beëindigt vervolgens het proces dat hij als slechtste beoordeelt; dit is vaak een database of uw SSH-sessie in plaats van de container die het probleem veroorzaakte. Limieten veranderen een volledige serverstoring in één service die opnieuw opstart.
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 bestandsindeling waarop dit voortbouwt.
deploy.resources.limits of mem_limit: welke is van toepassing
Er bestaan twee schrijfwijzen voor hetzelfde concept, wat de verwarring verklaart.
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. Docker Compose, 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 plugin bevindt zich nu in de v5-reeks (v5.6.0 was de meest recente stabiele release bij controle op 2026-10-10). Deze sprong van v2 direct naar v5, waarbij 3 en 4 werden overgeslagen om verwarring met de oude versies van het Compose-bestandsformaat te voorkomen; geen enkele v5-release heeft de afhandeling van resourcelimieten gewijzigd. De op Swarm gerichte onderdelen van het deploy-blok zijn de overige keys, zoals placement, update_config en endpoint_mode, die de planning en uitrol over een cluster van nodes voor docker stack deploy beschrijven. Het veelgehoorde advies dat "deploy Swarm vereist" is dus onjuist voor de resources-subsectie, en het opvolgen ervan zorgt ervoor dat uw services geen enkele limiet hebben.
Kies één schrijfwijze per project. Het gebruik van zowel mem_limit: 512m als deploy.resources.limits.memory: 1g op dezelfde service is niet alleen onleesbaar: de Compose Specification stelt dat wanneer mem_limit is ingesteld, dit consistent moet zijn met limits.memory, en er wordt niet vermeld welke waarde voorrang krijgt bij tegenstrijdigheden. Welke schrijfwijze u ook gebruikt, controleer wat de daemon daadwerkelijk heeft toegepast:
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 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 zit 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. Als dit onvoldoende ruimte 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 meestal dat iets anders een SIGKILL heeft verstuurd (een programma kan ook opzettelijk afsluiten met 137, maar dat is zeldzaam). De gebruikelijke oorzaak is dat docker compose stop de respijtperiode van tien seconden bereikt omdat de applicatie SIGTERM negeerde. Dit onderscheid bespaart uren werk, aangezien beide problemen niets met elkaar gemeen hebben.
Er zijn nog twee andere plekken waar de gebeurtenis wordt vastgelegd. 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). Die regel betekent dat één container zijn eigen limiet heeft bereikt; dit kan voorkomen terwijl de host nog voldoende vrij RAM-geheugen heeft. Een regel zonder het Memory cgroup-voorvoegsel duidt op een OOM op hostniveau, wat betekent dat de machine zelf geen RAM meer over heeft. Dat is de fout die limieten juist moeten voorkomen. Het zien hiervan betekent meestal dat de som van uw limieten te hoog is, dat sommige services helemaal geen limiet hebben, of dat iets buiten Docker meer verbruikt dan de marge die u heeft gereserveerd.
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 blijft sterven zichtbaar is zonder dat u er zelf toezicht op hoeft te houden.
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 strijd om resources of een tekort aan geheugen detecteert. Het voorkomt nooit dat een container hierboven uitkomt en garandeert nooit dat het geheugen vrij is wanneer de container erom vraagt. Het zorgt er enkel voor dat de kernel bij voorkeur geheugen terugvordert van containers die boven hun reservering zitten.
Een reservering biedt op zichzelf dus geen bescherming. Gebruik deze om een service aan te duiden die onder druk prioriteit moet krijgen, en vertrouw op de limiet voor de veiligheid. Houd de reservering onder de limiet, anders zal de container niet starten: Docker wijst de configuratie af 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, heeft elke onderstaande swap-instelling 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 realistisch over wat swap u oplevert. Het maakt een OOM-kill trager, niet minder waarschijnlijk, omdat een lekkend proces swap net zo gemakkelijk vult als RAM. Ondertussen vertraagt een container die swap gebruikt op gedeelde VPS-opslag elke andere service op de server. Voor alles wat latentiegevoelig is, zorgt een correcte limiet zonder swap ervoor dat fouten sneller en voorspelbaarder optreden.
Waarom geheugengebruik er slechter uitziet dan het is
Op Linux trekt het MEM USAGE-cijfer in docker stats alleen inactieve page cache af (inactive_file op cgroup v2). Recent gebruikte cache telt dus nog steeds mee, 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 tegen zijn plafond aan 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 dat wel kan. 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, waardoor ze niet werken 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 te verdelen; 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. Sommige runtimes negeren de limiet tenzij u ze expliciet instrueert, en de runtimes die de limiet wel lezen, schalen zichzelf mogelijk niet zoals u verwacht. PostgreSQL zal vrolijk shared_buffers en work_mem instellen voorbij de containerlimiet, waarna het proces wordt beëindigd. Een moderne JVM (Java virtual machine) detecteert de cgroup-limiet standaard, maar HotSpot-builds zoals OpenJDK geven de heap dan slechts 25 procent van die ruimte. Daarom is -XX:MaxRAMPercentage=75 de gebruikelijke manier om de heap meer van de limiet te laten gebruiken, terwijl er ruimte overblijft voor geheugen buiten de heap. Node.js schaalt de standaard heap sinds versie 12 op basis van de cgroup-limiet, op ongeveer de helft van de containerlimiet met een maximum van rond de 2GB. Stel --max-old-space-size daarom alleen in megabytes in als u een ander plafond wenst, en houd dit onder de containerlimiet zodat de garbage collector zijn werk kan doen voordat 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 (throttled) en gaat door, maar trager. Een CPU-limiet kan daarom agressief worden ingesteld, terwijl een geheugenlimiet ruimte voor pieken nodig heeft.
cpu_shares is een ander instrument: 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; op een systeem dat niet belast is, wordt geen van beide beperkt. Gebruik shares om services naar belangrijkheid 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. Docker Compose, de docker compose-plugin (inmiddels op de v5-lijn), 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>; dit commando toont de limiet in bytes en geeft 0 weer wanneer er geen limiet is ingesteld. De sleutels binnen deploy die met het oog op Swarm zijn geschreven, zijn de cluster-specifieke sleutels, zoals placement, update_config en endpoint_mode.
Wat betekent exitcode 137 in Docker Compose?
Dit betekent bijna altijd dat het hoofdproces een SIGKILL heeft ontvangen, aangezien 137 gelijk is aan 128 plus signaal 9 (een programma kan ook bewust afsluiten met 137, maar dat is zeldzaam). De OOM-killer van de kernel is de meest voorkomende oorzaak, maar een shutdown-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, terwijl false 137 dat niet doet.
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. De Compose Specification vereist dat beide consistent zijn wanneer ze beide zijn ingesteld; kies er dus één en verifieer het resultaat met docker inspect.
Waarom bereikt mijn container de volledige geheugenlimiet zonder te worden beëindigd?
Het verbruikscijfer in docker stats telt nog steeds recent gebruikte page cache mee, 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-I/O uitvoert, 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. Observeer 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.