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 service keys wey dey for top level, inherited from older Compose file formats. deploy.resources come from the Swarm schema and now e dey part of Compose Specification, wey be the format docker compose dey read today.
Both one dey work for single 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 now dey for v5 line (v5.6.0 na the latest stable release when dem check am on 2026-10-06). E jump from v2 straight go v5, skip 3 and 4 to avoid confusion with the old Compose file format versions, and no v5 release don change how e dey handle resource limits. The parts of deploy block wey only concern Swarm na the other keys: mode, placement, update_config and endpoint_mode get meaning for docker stack deploy, but docker compose up dey ignore dem. So the common advice say "deploy needs Swarm" no correct for the resources subsection, and if you follow am, your services no go get any 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, you go get file wey nobody fit understand quickly. Instead make you guess which number win, ask the daemon:
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 you see 0 for any field, e mean say no limit dey set. The smallest memory limit Docker fit accept na 6m, and if you set anything below am, the container no go start.
Wetin happen when container reach the limit
The container no go slow down. E go die.
When process request one page and cgroup don already reach its memory.max, kernel first 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 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, no be proof of OOM by itself.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 na OOM kill. false 137 mean say something else send SIGKILL, and the usual cause na docker compose stop reach its ten second grace period because the app ignore SIGTERM. This difference fit 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=oomThen 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 don run out of RAM. Na this kind failure limits suppose 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 look up for docker compose ps one second after e die. Check the uptime column and restart count, then pair the limit with healthcheck wey report the app as unhealthy 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
The MEM USAGE figure for docker stats include page cache, so a container wey dey read large files go climb reach e limit and remain there. Na normal behaviour, and e no be leak, because kernel fit reclaim clean cache before OOM killer ever run. 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.eventsanon 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 directly: if oom_kill counter dey above zero, kernel don kill something inside this container since e start, and if max counter dey climb, container dey reach e ceiling right now. Both commands need shell and coreutils inside the image, so dem go fail for distroless or scratch image.
Limits for 8GB VPS
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 wey you fit share, and the total of every container limit suppose stay below am. Overcommitting dey work until the day two services reach peak usage 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_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 am go be page cache.
No copy these 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 roughly half again as headroom. Limit wey you set too tight worse pass no limit, because e fit kill healthy service during normal traffic spike.
One trap need separate note. Most runtimes no dey see the limit unless you tell dem about am. PostgreSQL go happily size shared_buffers and work_mem above its container limit, then e go get killed. JVM (Java virtual machine) need -XX:MaxRAMPercentage=75 to size its heap from the cgroup limit instead of host RAM. Node.js need --max-old-space-size in megabytes, set below the container limit, or its garbage collector go let the heap grow until the kernel intervene. Ollama na the same situation with another setting, because raising num_ctx dey grow the KV cache by hundreds of megabytes and the container go die halfway through a 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
Does deploy.resources.limits work without Docker Swarm?
Yes. Docker Compose, the docker compose plugin (now e dey for v5 line), 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 go print the limit for bytes and print 0 when no limit apply. The keys inside deploy wey genuinely need Swarm na mode, placement, update_config and endpoint_mode.
Wetin exit code 137 mean for Docker Compose?
E mean say main process receive SIGKILL, because 137 na 128 plus signal 9. Kernel OOM killer na the common cause, but shutdown timeout go produce the same code when app ignore SIGTERM. Run docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> to know the difference. true 137 mean say memory kill happen, while false 137 no mean that one.
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 form, 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. Setting both for one service go only make the file harder to read, so pick one and verify the result with docker inspect.
Why my container dey use the full memory limit without getting killed?
The usage figure for docker stats include page cache. 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. 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, no be container wey dey about 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 sum 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 numbers. Treat the total as a budget, no be target wey you must fill.