Docker Compose-geheugenlimiet instellen tegen exit 137
Stel geheugen- en CPU-limieten in Docker Compose in. Voorkom dat een container uw VPS overbelast en leer exit 137, swap en deploy.resources correct lezen.
Wat een Docker Compose-geheugenlimiet doet
Een Docker Compose-geheugenlimiet is een harde bovengrens die de Linux-kernel instelt voor de cgroup (control group, de kernelvoorziening die resources voor een groep processen meet) van één container. Stel deploy.resources.limits.memory in voor een service. Die container kan dan nooit meer gebruiken dan de opgegeven waarde. Wanneer de container die grens bereikt, beëindigt de kernel een proces in de container. De container wordt vervolgens meestal afgesloten met exitcode 137.
Dit is vooral belangrijk op een VPS, waar de hoeveelheid RAM vaststaat en geen extra geheugen van de host beschikbaar is. Eén container met een geheugenlek of een slechte query kan elke vrije geheugenpagina op een machine met 8GB innemen. De kernel beëindigt dan het proces dat volgens de kernel het meest problematisch is. Dat is vaak een database of uw SSH-sessie, en niet de container die het probleem heeft veroorzaakt. Limieten beperken een storing op de hele server tot één service die opnieuw wordt gestart.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mPas de instelling toe en controleer of de limiet actief is:
docker compose up -d
docker stats --no-streamDe kolom MEM USAGE / LIMIT moet ongeveer 142MiB / 1GiB weergeven. Als de limietkolom het volledige RAM van de host toont, is de instelling niet toegepast. De rest van deze handleiding helpt dan niet totdat dit is opgelost. Als het compose-bestand nieuw voor u is, behandelen de basisprincipes van Docker Compose voor een VPS de bestandsindeling waarop deze configuratie voortbouwt.
deploy.resources.limits of mem_limit: welke is van toepassing
Er bestaan 2 schrijfwijzen voor hetzelfde concept. Daarom is dit verwarrend.
mem_limit, mem_reservation, memswap_limit, cpus en cpu_shares zijn service-sleutels op het hoogste niveau die zijn overgenomen uit oudere Compose-bestandsindelingen. deploy.resources is afkomstig uit het Swarm-schema en maakt nu deel uit van de Compose-specificatie. Dit is de indeling die docker compose tegenwoordig leest.
Beide werken op 1 host. Compose V2, de docker compose-plugin, past deploy.resources.limits en deploy.resources.reservations toe wanneer u docker compose up uitvoert, zonder dat ergens een Swarm-cluster nodig is. De Swarm-specifieke onderdelen van het blok deploy zijn de andere sleutels: mode, placement, update_config en endpoint_mode hebben betekenis voor docker stack deploy en worden door docker compose up genegeerd. Het algemene advies dat "deploy Swarm vereist" is daarom onjuist voor de resources-subsectie. Als u dit advies volgt, hebben uw services helemaal geen limiet.
Kies per project 1 schrijfwijze. Als u mem_limit: 512m en deploy.resources.limits.memory: 1g voor dezelfde service opgeeft, ontstaat een bestand waarvan niet direct duidelijk is welke waarde geldt. Raad niet welke waarde is toegepast, maar vraag het aan de daemon:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Geheugenwaarden worden uitgedrukt in bytes. Daarom geeft 1g de waarde 1073741824 weer. CPU wordt uitgedrukt in nano-CPU's. Daarom geeft 1.5 de waarde 1500000000 weer. Een 0 in een veld betekent dat er geen limiet is ingesteld. De kleinste geheugenlimiet die Docker accepteert is 6m. Onder deze waarde start de container niet.
Wat gebeurt er wanneer een container de limiet bereikt
De container wordt niet trager. Hij stopt.
Wanneer een proces een pagina aanvraagt en de cgroup al op memory.max staat, maakt de kernel eerst vrij wat binnen die cgroup kan worden vrijgemaakt: schone page cache en daarna pagina's die naar swap kunnen worden verplaatst. Als dat niet genoeg geheugen vrijmaakt, selecteert de OOM-killer (out of memory) van de cgroup een proces binnen de container en stuurt het SIGKILL. Als PID 1 van de container wordt beëindigd, eindigt de container. Exitcode 137 is eenvoudigweg 128 plus signaal 9. Daarom is 137 de kenmerkende waarde van elke SIGKILL, maar op zichzelf geen bewijs van een OOM.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 is een OOM-kill. false 137 betekent dat iets anders SIGKILL heeft verzonden. De gebruikelijke oorzaak is dat docker compose stop de respijtperiode van tien seconden bereikt omdat de app SIGTERM negeerde. Dit onderscheid bespaart uren, omdat de twee problemen niets met elkaar te maken hebben.
Twee andere plaatsen leggen de gebeurtenis vast. Controleer de daemon live:
docker events --filter event=oomLees daarna het kernellogboek. Dit is het logboek dat na een herstart behouden blijft:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Een cgroup-kill schrijft een regel die begint met Memory cgroup out of memory: Killed process 24713 (node). Een regel zonder het voorvoegsel Memory cgroup is een host-OOM. Dit betekent dat de machine zelf geen RAM meer had. Dat is precies de fout die limieten moeten voorkomen. Als u dit ziet, zijn de limieten samen te hoog, of hebben sommige services helemaal geen limiet.
Met restart: unless-stopped blijft een OOM-lus moeilijk zichtbaar, omdat de service één seconde nadat deze is beëindigd alweer actief lijkt in docker compose ps. Controleer de uptime-kolom en het aantal herstarts. Combineer de limiet met een healthcheck die de app als ongezond rapporteert, zodat een container die steeds stopt zichtbaar blijft zonder dat u deze voortdurend hoeft te controleren.
Een reservering is een aanwijzing; de limiet is de regel
reservations.memory (de oudere mem_reservation) is een zachte ondergrens. Docker beschrijft deze als een zachte limiet die wordt geactiveerd wanneer de daemon concurrentie om resources of weinig geheugen op de host detecteert. De limiet verhindert nooit dat een container erboven komt en garandeert nooit dat het geheugen beschikbaar is wanneer de container erom vraagt. De limiet zorgt er alleen voor dat de kernel eerst geheugen probeert terug te winnen van containers die boven hun reservering zitten.
Een reservering beschermt dus op zichzelf niets. Gebruik deze om een service aan te merken die onder druk met voorrang moet worden behandeld en vertrouw voor de veiligheid op de limiet. Houd de reservering onder de limiet, anders start de container niet: Docker weigert de configuratie met Minimum memory limit can not be less than memory reservation limit.
Eerlijke swapboekhouding
De meeste VPS-images worden geleverd zonder swapbestand. Voer swapon --show en free -h uit. Als de totale swapcapaciteit nul is, heeft geen enkele swapgerelateerde instelling hieronder effect en is uw geheugenlimiet uitsluitend een RAM-limiet.
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. Als u beide waarden gelijk instelt, krijgt de container helemaal geen swap. Als u mem_limit instelt en memswap_limit niet instelt, kan de container opnieuw swap gebruiken tot de grootte van zijn geheugenlimiet.
Ubuntu 24.04 en Debian 13 gebruiken standaard cgroup v2. Daarin is swap een afzonderlijke teller (memory.swap.max) en werkt dit zonder extra configuratie. De oude melding Your kernel does not support swap limit capabilities is afkomstig van hosts met cgroup v1 die zonder swapaccount=1 zijn opgestart. Op deze hosts blijft de geheugenlimiet van toepassing, terwijl het swapgedeelte wordt genegeerd.
Wees duidelijk over wat swap oplevert. Swap zorgt ervoor dat een OOM-kill langer duurt, maar niet dat deze minder waarschijnlijk wordt. Een proces met een geheugenlek vult swap net zo gemakkelijk als RAM. Ondertussen vertraagt een container die swap intensief gebruikt op gedeelde VPS-opslag alle andere services op de host. Voor alles waarbij latency belangrijk is, faalt een juiste limiet zonder swap sneller en voorspelbaarder.
Waarom het geheugengebruik erger lijkt dan het is
De waarde MEM USAGE in docker stats omvat ook de paginacache. Daarom loopt een container die grote bestanden leest op tot zijn limiet en blijft daar staan. Dat is normaal en geen geheugenlek, omdat schone cache wordt vrijgegeven voordat de OOM-killer ooit wordt aangeroepen. Een service zoals een zelf gehoste Jellyfin-mediaserver lijkt om precies deze reden permanent bijna aan zijn limiet te zitten.
Splits het getal vanuit de container op 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 vrijgegeven. file is paginacache, die wel kan worden vrijgegeven. Stel de limiet af op anon plus een marge, niet op het totaal. Het bestand memory.events neemt de twijfel volledig weg: een teller oom_kill groter dan nul betekent dat de kernel iets in deze container heeft beëindigd sinds de container is gestart. Een oplopende teller max betekent dat de container momenteel tegen zijn limiet wordt gehouden. Voor beide opdrachten zijn een shell en coreutils in de image nodig. Daarom mislukken ze op een distroless- of scratch-image.
Limieten voor sizing op een VPS van 8GB
Begin bij de host, niet bij de applicaties. Reserveer op een VPS van 8GB ongeveer 1GB voor de kernel, de Docker daemon, sshd, journald en uw eigen login-shell. Dan blijft er ongeveer 7GB over om toe te wijzen. De som van alle containerlimieten moet daaronder blijven. Overcommitting werkt totdat twee services tegelijk hun piekbelasting bereiken.
Een werkbare verdeling op een systeem van 8GB:
- Reverse proxy: limiet van 128m. Dit is een klein proces. Met zo'n krappe limiet wordt een runaway-configuratieherlading direct gedetecteerd.
- PostgreSQL: limiet van 2g, met
shared_buffersingesteld op ongeveer 512MB in de databaseconfiguratie. - Applicatiecontainer: limiet van 1g.
- Achtergrondworker: limiet van 512m.
- Media- of bestandsservice: limiet van 2g. Het grootste deel daarvan wordt page cache.
Neem deze waarden niet zonder meer over in uw eigen stack. Laat de services een dag onder realistische belasting draaien. Monitor docker stats, noteer de piekwaarde van anon per container en tel daar ongeveer de helft bij op als headroom. Een te krappe limiet is erger dan geen limiet, omdat daarmee een gezonde service tijdens een normale piek in het netwerkverkeer wordt beëindigd.
Eén valkuil verdient een eigen opmerking. De meeste runtimes kennen de limiet niet automatisch. U moet de limiet aan hen doorgeven. PostgreSQL stelt shared_buffers en work_mem zonder problemen hoger in dan de containerlimiet en wordt daarna beëindigd. Een JVM (Java virtual machine) heeft -XX:MaxRAMPercentage=75 nodig om zijn heap af te stemmen op de cgroup-limiet in plaats van op het RAM-geheugen van de host. Node.js heeft --max-old-space-size nodig in megabytes. Stel deze waarde lager in dan de containerlimiet. Anders laat de garbage collector de heap groeien totdat de kernel ingrijpt. De cgroup onderhandelt niet. Deze beëindigt het proces.
CPU-limieten werken heel anders
cpus: "1.5" betekent 150% van één core, afgedwongen als een CFS-quota (completely fair scheduler). De container krijgt in elke periode van 100ms 150ms CPU-tijd, gedeeld door al zijn threads. Wanneer die tijd is verbruikt, laat de kernel de container wachten tot de volgende periode.
Dat is het belangrijke verschil. Een container die zijn geheugenlimiet overschrijdt, wordt beëindigd. Een container die zijn CPU-limiet overschrijdt, wordt afgeremd en gaat verder, maar langzamer. Daarom kunt u een CPU-limiet relatief hoog instellen, terwijl een geheugenlimiet voldoende marge nodig heeft.
cpu_shares is een ander hulpmiddel: een relatief gewicht dat alleen relevant is wanneer de CPU's daadwerkelijk volledig belast zijn. Twee containers met shares van 1024 en 512 verdelen een drukbezette core ongeveer in een verhouding van twee op één. Op een niet-belaste server wordt geen van beide beperkt. Gebruik shares om services op belangrijkheid te rangschikken en gebruik cpus wanneer u een echte bovengrens nodig hebt, bijvoorbeeld om te voorkomen dat een nachtelijke transcodeertaak uw webserver zonder CPU-tijd laat.
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 op één host uitvoert. Controleer dit met docker inspect --format '{{.HostConfig.Memory}}' <container>. Hiermee wordt de limiet in bytes weergegeven en 0 wanneer geen limiet is toegepast. De sleutels in deploy waarvoor Swarm daadwerkelijk vereist is, zijn mode, placement, update_config en endpoint_mode.
Wat betekent exit code 137 in Docker Compose?
Dit betekent dat het hoofdproces SIGKILL heeft ontvangen, omdat 137 gelijk is aan 128 plus signaal 9. De OOM-killer van de kernel is de meest voorkomende oorzaak, maar een afsluit-time-out produceert dezelfde code wanneer een applicatie SIGTERM negeert. Voer docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> uit om het verschil vast te stellen. true 137 is een geheugenkill 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 de oudere sleutels op topniveau gebruikt. Als u beide voor één service instelt, wordt het bestand alleen moeilijker leesbaar. Kies daarom één optie en controleer het resultaat met docker inspect.
Waarom gebruikt mijn container de volledige geheugenlimiet zonder te worden beëindigd?
Het gebruikscijfer in docker stats omvat de page cache. De kernel laat die onder druk vallen zonder een OOM-kill te activeren. Voer docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat uit en lees de waarde anon. Dit is de actieve werkset die niet kan worden vrijgegeven. Een hoge waarde voor file naast een lage waarde voor anon betekent dat de container schijfinput en -output uitvoert, niet dat de container op het punt staat te worden beëindigd.
Hoeveel RAM moet ik ongealloceerd laten op een VPS van 8GB?
Laat ongeveer 1GB over voor de kernel, de Docker-daemon, sshd, journald en uw eigen shell. Houd vervolgens de som van alle containerlimieten onder de resterende 7GB. Controleer gedurende een dag onder realistische belasting de piekwaarde anon per container voordat u de waarden definitief vastlegt. Beschouw het totaal als een budget en niet als een doel dat u volledig moet benutten.