Docker Compose memory limit wey stop OOM for VPS
Set Docker Compose memory and CPU limits so one container no go crash your VPS. Learn deploy.resources, mem_limit, exit 137, swap, and correct sizing.
Wetín Docker Compose memory limit dey do
Docker Compose memory limit na hard cap wey Linux kernel put for one container cgroup (control group, na kernel feature wey dey measure resources for set of processes). Set deploy.resources.limits.memory for service, and that container no fit ever use pass the number wey you write. When e try, kernel go kill one process inside the container, and the container usually go exit with code 137.
This one matter 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 bad pass, and many times na database or your SSH session e go kill instead of the container wey cause the problem. Limits turn whole-server outage into one service wey fit restart.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mApply am and confirm say the limit dey active:
docker compose up -d
docker stats --no-streamThe 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 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.
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, na the format wey docker compose dey read today.
Both dey work for one host. Docker Compose, 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 plugin dey now use v5 line (v5.6.0 na the latest stable release when dem check am on 2026-10-10). E jump from v2 straight to v5, skip 3 and 4 to avoid confusion with the old Compose file format versions, and no v5 release change how resource limits dey work. The Swarm-focused parts of the deploy block na the other keys, like placement, update_config and endpoint_mode, wey describe scheduling and rollouts across cluster of nodes for docker stack deploy. So the common advice say "deploy needs Swarm" no correct for the resources subsection, and 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, e no just hard to read: the Compose Specification talk say when mem_limit dey set, e must agree with limits.memory, but e no talk which value go win when dem no agree. Any spelling wey you use, confirm wetin the daemon actually apply:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Memory values na bytes, so 1g go print as 1073741824. CPU dey use nano CPUs, so 1.5 go print as 1500000000. If 0 dey for any field, e mean say no limit dey set. The smallest memory limit Docker accept na 6m, and if you set anything below am, the container no go start.
Wetín dey happen when container reach the limit
Container no go slow down. E go die.
When process ask for page and cgroup don already reach its memory.max, kernel first dey reclaim wetin e fit inside that cgroup: clean page cache, then pages wey e fit swap. If reclaim no free enough memory, cgroup OOM (out of memory) killer go pick one process inside container and send am SIGKILL. If e kill container PID 1, container go end. Exit code 137 na simply 128 plus signal 9, so 137 na fingerprint of any SIGKILL; e no prove say OOM happen by itself.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 na OOM kill. false 137 usually mean say something else send SIGKILL (program fit also exit with 137 on purpose, but that one rare), and the usual cause na docker compose stop reach its ten second grace period because app ignore SIGTERM. This difference fit save hours, because the two problems no get anything in common.
Two other places dey record the event. Monitor the daemon live:
docker events --filter event=oomThen read kernel log, wey be the record wey survive restart:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Cgroup kill dey print line wey start with Memory cgroup out of memory: Killed process 24713 (node). That line mean say one container reach its own limit, and e fit show while host still get plenty free RAM. Line wey no get Memory cgroup prefix na host OOM, meaning say the machine itself don run out of RAM. Na this failure limits suppose prevent, so if you see am, e usually mean say sum of your limits too high, some services no get limit at all, or something outside Docker dey use more memory than the headroom wey you leave.
With restart: unless-stopped, OOM loop fit hide well, because service go look up inside docker compose ps one second after e die. Check uptime column and restart count, then join the limit with healthcheck wey report say app no healthy so container wey dey die again and again go show without you watching am.
Reservation na hint, limit na rule
reservations.memory (the older mem_reservation) na soft floor. Docker describe am as soft limit wey dey activate when daemon detect contention or low memory for host. E no ever stop container from going pass am, and e no guarantee say the memory go dey free when container request am. E only bias kernel make e reclaim memory first from containers wey dey above their reservation.
So reservation no protect anything by itself. Use am to mark service wey you want system treat well when pressure dey, then 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, honestly
Most VPS images dey release without any swap file at all. 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 get 1GB of RAM and 1GB of swap. If you set the two values equal, the container no get swap at all. If you set mem_limit and leave memswap_limit unset, the container fit swap up to the size of its memory limit again.
Ubuntu 24.04 and Debian 13 dey use cgroup v2 by default, where swap dey use separate counter (memory.swap.max), and this one work without extra setup. 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 apply, but the swap part dey ignored.
Make you dey honest about wetin swap dey give you. E dey make OOM kill slower, not less likely, because process wey dey leak memory go fill swap just as e dey fill RAM. Meanwhile, container wey dey thrash swap for shared VPS storage go slow every other service for the machine. For anything wey latency matter, correct limit without swap go fail faster and more predictably.
Why memory usage dey look worse than e be
For Linux, the MEM USAGE figure for docker stats only subtract inactive page cache (inactive_file for cgroup v2), so cache wey dem recently use still dey count. Container wey dey read large files go climb reach its limit and remain there. This one normal, and e no be leak, because kernel go reclaim clean cache before e ever call OOM killer. Service like self-hosted Jellyfin media server go look like e dey permanently near its ceiling exactly because of this.
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.eventsanon na anonymous memory, the working set wey kernel no fit drop. file na page cache, wey kernel fit drop. Set your limit based on anon plus some margin, no be on the total. The memory.events file settle the matter directly: if oom_kill counter dey above zero, kernel don kill something for this container since e start. If max counter dey climb, container dey hit its ceiling right now. Both commands need shell and coreutils inside the image, so dem go fail for distroless or scratch image.
Limits wey 8GB VPS fit handle
Start from the host, no be from the apps. For 8GB VPS, leave about 1GB for the 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 fit work until the day two services reach peak usage together.
This na workable split for 8GB box:
- Reverse proxy: 128m limit. E small process, and this tight limit go catch runaway config reload immediately.
- PostgreSQL: 2g limit, with
shared_buffersset to about 512MB for the database config. - Application container: 1g limit.
- Background worker: 512m limit.
- Media or file service: 2g limit, and most of that memory go serve as page cache.
No copy these numbers enter your own stack. Run the services under real load for one day, monitor docker stats, record the peak anon value for each container, then add roughly half again as headroom. Limit wey too tight worse than no limit, because e fit kill healthy service during normal traffic spike.
One trap need separate note. Some runtimes ignore the limit unless you tell dem about am, and the ones wey read am fit still size themselves differently from wetin you expect. PostgreSQL fit size shared_buffers and work_mem pass the container limit, then e go get killed. Modern JVM (Java virtual machine) detects the cgroup limit by default, but HotSpot builds like OpenJDK then give the heap only 25 percent of am. So -XX:MaxRAMPercentage=75 na the usual way to let the heap use more of the limit while e still leave room for memory outside the heap. Node.js don dey size e default heap from the cgroup limit since version 12, at about half the container limit and e dey level off around 2GB. So set --max-old-space-size for megabytes only when you want different ceiling, and keep am below the container limit so garbage collector fit work before kernel intervene. Ollama na the same situation with different setting, because raising num_ctx dey grow KV cache by hundreds of megabytes and the container go die halfway through long prompt. The cgroup no dey negotiate. E dey kill.
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 get 150ms CPU time for every 100ms period, shared across all im threads. Once e use up the time, kernel go make am wait for the next period.
Na this contrast dey important. If container pass im memory limit, dem go kill am. If container pass im CPU limit, dem go throttle am and e go 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 really dey saturated. Two containers wey get shares of 1024 and 512 go share one busy core roughly two to one. For idle machine, dem no go restrict either one. Use shares to rank services by importance, and use cpus when you need real ceiling, like to stop nightly transcode job from starving your web server.
FAQ
E deploy.resources.limits dey work without Docker Swarm?
Yes. Docker Compose and the docker compose plugin (wey dey on v5 line now) 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>. E go print the limit for bytes, and e go print 0 when no limit apply. The keys inside deploy wey dem write with Swarm for mind na the cluster ones, like placement, update_config and endpoint_mode.
Wetin exit code 137 mean for Docker Compose?
E almost always mean say the main process receive SIGKILL, because 137 na 128 plus signal 9. (Program fit still exit with 137 deliberately, but that one rare.) 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, while false 137 no be.
I suppose set mem_limit or deploy.resources.limits.memory?
Either one dey work with docker compose. deploy.resources.limits.memory na the current Compose Specification format, and e better as default for new file. Keep mem_limit if the rest of your file already dey use the older top-level keys. Compose Specification require both values to agree when you set both, 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 still dey count page cache wey dem use recently. Kernel dey drop this cache when memory pressure dey, instead of triggering OOM kill. Run docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat and check the anon value. Na the working set wey kernel no fit reclaim. High file value beside low anon value mean say container dey do disk input and output. E no mean say container dey close to die.
How much RAM I suppose leave unallocated for 8GB VPS?
Leave about 1GB for the kernel, Docker daemon, sshd, journald and your own shell. Then keep the sum of all container limits below the remaining 7GB. Watch the peak anon value for each container under real load for one day before you settle on the numbers. Treat the total as a budget, no be target wey you must fill.