SSD Nodes Learn 8GB RAM — $66/yr
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-02

Docker Compose memory limit wey stop OOM for VPS

Set memory and CPU limits for Docker Compose. Learn deploy.resources, mem_limit, exit 137, swap, and how to size containers so VPS no crash.

Docker Compose memory limit dey do wetin

Docker Compose memory limit na hard cap wey Linux kernel dey put for one container cgroup (control group, na kernel feature wey dey measure resources for group of processes). Set deploy.resources.limits.memory for service, and that container no fit use pass the number wey you write. When e try, kernel go kill one process inside the container, and container usually go exit with code 137.

This one important pass for VPS, where RAM fixed and no extra host memory dey to borrow. One container wey get memory leak or bad query fit use every free page for 8GB machine. Kernel go then kill any process wey e judge say na the worst one, and many times na database or your SSH session instead of the container wey cause the problem. Limits dey turn whole-server outage into one service wey go restart.

services:
  app:
    image: ghcr.io/example/app:1.4
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 256m

Apply am and confirm say the limit dey active:

docker compose up -d
docker stats --no-stream

The MEM USAGE / LIMIT column suppose show something like 142MiB / 1GiB. If the limit column show the full host RAM, the setting no apply, and the rest of this guide no go help until e apply. If compose file still new to you, Docker Compose basics for a VPS explain the file layout wey this guide dey build on.

deploy.resources.limits or mem_limit: which one dey apply

Two spellings dey for the same idea, na why this matter dey confuse people.

mem_limit, mem_reservation, memswap_limit, cpus and cpu_shares na top-level service keys wey older Compose file formats inherit. deploy.resources come from the Swarm schema, and e don now become part of the Compose Specification, wey docker compose dey read today.

Both dey work for one host. Compose V2, the docker compose plugin, dey apply deploy.resources.limits and deploy.resources.reservations when you run docker compose up, even when no Swarm cluster dey anywhere. The Swarm-only parts of the deploy block na the other keys: mode, placement, update_config and endpoint_mode mean something to docker stack deploy, but docker compose up dey ignore dem. So the common advice say “deploy needs Swarm” no correct for the resources subsection. If you follow am, your services go get no limit at all.

Choose one spelling for each project. If you write mem_limit: 512m and deploy.resources.limits.memory: 1g for the same service, the file go hard to understand at a glance. Instead of guessing which number win, ask the daemon:

docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1

Memory values na bytes, so 1g go print as 1073741824. CPU dey use nano CPUs, so 1.5 go print as 1500000000. 0 for any field mean say no limit dey set. The smallest memory limit wey Docker accepts na 6m, and if you set anything below am, the container no go start.

Wetin dey happen when container reach the limit

The container no go slow down. E go die.

When process request page and cgroup don already reach its memory.max, the kernel first reclaim wetin e fit inside that cgroup: clean page cache, then pages wey e fit swap. If reclaim no free enough space, the cgroup OOM (out of memory) killer go pick process inside the container and send am SIGKILL. If e kill the container PID 1, the container go end. Exit code 137 na simply 128 plus signal 9, so 137 na fingerprint of any SIGKILL, e no prove OOM by itself.

docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1

true 137 mean say OOM kill happen. false 137 mean say another thing send SIGKILL, and the usual cause na docker compose stop reach its ten second grace period because the app ignore SIGTERM. This difference save hours, because the two problems no get anything to do with each other.

Two more places dey record the event. Monitor the daemon live:

docker events --filter event=oom

Then read the kernel log, wey be the record wey survive restart:

sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'

A cgroup kill go print line wey start with Memory cgroup out of memory: Killed process 24713 (node). Line wey no get the Memory cgroup prefix na host OOM, meaning say the machine itself run out of RAM. Na this failure limits dey prevent, so if you see am, e show say the total of your limits too high, or some services no get limit at all.

With restart: unless-stopped, OOM loop fit hide well, because the service show up in docker compose ps one second after e die. Check the uptime column and the restart count, then pair the limit with healthcheck wey report say app no healthy so container wey dey die again and again go dey visible without you watching am.

Reservation na hint, limit na rule

reservations.memory (the old mem_reservation) na soft floor. Docker describe am as soft limit wey e go activate when daemon detect contention or low memory for the host. E no dey ever stop container from passing am, and e no dey ever guarantee say memory go dey free when container ask for am. E only dey push kernel make e reclaim memory first from containers wey dey above their reservation.

So, reservation no dey protect anything by itself. Use am mark service wey you want make dem treat well when pressure dey, and depend on limit for safety. Keep reservation below limit, otherwise container no go start: Docker go reject the config with Minimum memory limit can not be less than memory reservation limit.

Swap accounting dey honest

Most VPS images dey come without any swap file. Run swapon --show and free -h. If the swap total na zero, every swap-related setting below no go do anything, and your memory limit na pure RAM cap.

memswap_limit no be the amount of swap. E be the total of memory plus swap. With mem_limit: 1g and memswap_limit: 2g, the container go get 1GB RAM and 1GB swap. If you set the two values equal, the container no go get any swap. If you set mem_limit and leave memswap_limit unset, the container fit swap reach the size of its memory limit again.

Ubuntu 24.04 and Debian 13 dey use cgroup v2 by default. For this version, swap na separate counter (memory.swap.max), and this setup works without extra configuration. The old message Your kernel does not support swap limit capabilities dey come from cgroup v1 hosts wey boot without swapaccount=1. For those hosts, the memory limit still dey apply, but the swap part dey ignored.

Make you talk true about wetin swap fit do. E dey make OOM kill slower, but e no make am less likely, because a leaking process go fill swap just as e dey fill RAM. At the same time, a container wey dey thrash swap for shared VPS storage go slow down every other service for the box. For anything wey latency matter, correct limit without swap go fail faster and in a more predictable way.

Why memory usage looks worse than it is

The MEM USAGE figure wey dey inside docker stats include page cache, so container wey dey read big files go climb reach e limit and remain there. This one normal, and e no be leak, because kernel dey reclaim clean cache before e ever call OOM killer. Service like self-hosted Jellyfin media server go look like e dey permanently near e ceiling for exactly this reason.

Split the number into cache and real working set from inside the container:

docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.events

anon na anonymous memory, the working set wey kernel no fit drop. file na page cache, wey kernel fit reclaim. Set your limit based on anon plus small margin, no be based on the total. The memory.events file go settle the matter completely: if oom_kill counter dey above zero, e mean say kernel don kill something inside this container since e start, and if max counter dey increase, e mean say container dey hit e ceiling now. Both commands need shell and coreutils inside the image, so dem go fail for distroless or scratch image.

Sizing limits for 8GB VPS

Start from the host, no be from the apps. For 8GB VPS, leave about 1GB for kernel, Docker daemon, sshd, journald and your own login shell. That one leave roughly 7GB to share, and the total of every container limit suppose stay under am. Overcommitting dey work until the day two services peak together.

One workable split for 8GB box:

  • Reverse proxy: 128m limit. Na small process, and this tight limit go catch runaway config reload immediately.
  • PostgreSQL: 2g limit, with shared_buffers set to about 512MB for the database config.
  • Application container: 1g limit.
  • Background worker: 512m limit.
  • Media or file service: 2g limit, and most of am go be page cache.

No copy those numbers enter your own stack. Run the services under real load for one day, monitor docker stats, take the peak anon value for each container and add about half again as headroom. Limit wey too tight worse pass no limit, because e fit kill healthy service during normal traffic spike.

One trap need special note. Most runtimes no dey see the limit unless you tell dem about am. PostgreSQL go size shared_buffers and work_mem past the container limit and get killed. A JVM (Java virtual machine) needs -XX:MaxRAMPercentage=75 to size its heap from the cgroup limit instead of host RAM. Node.js needs --max-old-space-size in megabytes, set below the container limit, or its garbage collector go let the heap grow until kernel intervene. The cgroup no dey negotiate. E kills.

CPU limits dey behave completely different

cpus: "1.5" mean 150% of one core, and CFS (completely fair scheduler) quota dey enforce am. The container dey get 150ms CPU time inside every 100ms period, shared across all im threads. When e don use all of am, the kernel go make am wait for the next period.

Na this contrast important. If container pass im memory limit, e dey get killed. If container pass im CPU limit, e dey get throttled and continue, but more slowly. So you fit set CPU limit aggressively without much risk, but memory limit need headroom.

cpu_shares na different tool: relative weight wey only matter when CPUs don really saturate. Two containers wey get shares of 1024 and 512 go share one busy core roughly two to one, and if box dey idle, none of dem go get restriction. Use shares to rank services based on importance, and use cpus when you need real ceiling, like to stop nightly transcode job from starving your web server.

FAQ

deploy.resources.limits dey work without Docker Swarm?

Yes. Compose V2 dey apply deploy.resources.limits and deploy.resources.reservations when you run docker compose up for one host. Confirm am with docker inspect --format '{{.HostConfig.Memory}}' <container>, wey dey print the limit for bytes and print 0 when no limit apply. The keys inside deploy wey really need Swarm na mode, placement, update_config and endpoint_mode.

Wetin exit code 137 mean for Docker Compose?

E mean say the main process receive SIGKILL, because 137 na 128 plus signal 9. The kernel OOM killer na the common cause, but shutdown timeout fit produce the same code when app ignore SIGTERM. Run docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> to know the difference. true 137 na memory kill, but false 137 no be.

I suppose set mem_limit or deploy.resources.limits.memory?

Either one go work with docker compose. deploy.resources.limits.memory na the current Compose Specification format and na the better default for new file. Keep mem_limit if the rest of your file already dey use the older top-level keys. If you set both for one service, e only make the file harder to read, so choose one and verify the result with docker inspect.

Why my container dey stay for the full memory limit without getting killed?

The usage figure for docker stats include page cache. The kernel dey drop this cache when memory pressure happen instead of triggering OOM kill. Run docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat and read the anon value. This one na the working set wey the system no fit reclaim. High file value beside low anon value mean say the container dey do disk input and output, not say e dey near to die.

How much RAM I suppose leave unallocated for an 8GB VPS?

Leave about 1GB for the kernel, Docker daemon, sshd, journald and your own shell. Then keep the total of all container limits below the remaining 7GB. Monitor the peak anon value for each container under real load for one day before you commit to the numbers. Treat the total as a budget, not as a target wey you must fill.