Docker Compose memory limit wey stop OOM for VPS
Set Docker Compose memory limits so one container no go crash your VPS. Learn deploy.resources, mem_limit, exit 137, swap, and how to size am.
Wetin 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 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 matter dey important pass for VPS, where RAM dey fixed and host memory no dey extra 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 im worst, and many times na database or your SSH session 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 limit column show the full host RAM, the setting no apply, and the remaining part 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 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 for top level wey older Compose file formats inherit. deploy.resources come from Swarm schema, and e don now become part of 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 parts wey only concern Swarm for 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 resources subsection. If you follow am, your services fit get no limit at all.
Pick one spelling for each project. If you write mem_limit: 512m and deploy.resources.limits.memory: 1g for the same service, nobody fit understand the file quickly. Instead make you guess which number take effect, ask the daemon:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Memory values dey 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 set. The smallest memory limit wey Docker accept na 6m, and if you set anything below am, the container no go start.
Wetín happen when container reach the limit
The container no go slow down. E go die.
When process ask for 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 space, 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 simply mean 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-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 in common.
Two other places dey record the event. Monitor the daemon live:
docker events --filter event=oomThen read the kernel log, wey na 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 failure limits suppose prevent, so if you see am, e mean 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 inside docker compose ps one second after e die. Check the uptime column and restart count, then pair the limit with healthcheck wey report say the app no healthy so container wey dey die repeatedly go dey visible without you watching am.
Reservation na hint, limit na the rule
reservations.memory (the older mem_reservation) na soft floor. Docker describe am as soft limit wey activate when daemon detect contention or low memory for host. E no ever stop container from going above am, and e no ever guarantee say memory go dey free when container ask for am. E only dey direct kernel make e reclaim first from containers wey dey above their reservation.
So reservation no dey protect anything by itself. Use am to mark service wey you want make system treat gently 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, real talk
Most VPS images dey come without any swap file. Run swapon --show and free -h. If the total swap na zero, every swap setting below no go do anything, and your memory limit na RAM cap only.
memswap_limit no be the amount of swap. Na the total memory plus swap. With mem_limit: 1g and memswap_limit: 2g, the container get 1GB RAM and 1GB 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 use swap up to the size of its memory limit again.
Ubuntu 24.04 and Debian 13 dey use cgroup v2 by default. For there, swap get 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 dem ignore the swap part.
Make you understand wetin swap fit do. E make OOM kill slower, but e no make am less likely. A process wey dey leak memory go fill swap just as e dey fill RAM. At the same time, container wey dey thrash swap for shared VPS storage go slow down every other service for the server. For anything wey latency matter, correct limit without swap go fail faster and with more predictable result.
Why memory usage dey look worse than e be
The MEM USAGE figure for docker stats include page cache, so container wey dey read big files go climb reach its limit and remain there. Na normal behaviour be this, and e no be leak, because kernel fit 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 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 no fit drop. file na page cache, wey fit drop. Set your limit based on anon plus small extra margin, no be based on the total. The memory.events file settle the matter clearly: 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 hit its ceiling right now. Both commands need shell and coreutils inside the image, so dem go fail for distroless or scratch image.
Sizing limits for one 8GB VPS
Start from the host, no be from the apps. For one 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 all container limits suppose stay below am. Overcommitting fit work until the day two services reach peak usage together.
One workable split for one 8GB box:
- Reverse proxy: 128m limit. E be 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, record the peak anon value for each container, then add roughly 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 happily size shared_buffers and work_mem pass the container limit, then e go get killed. A JVM (Java virtual machine) need -XX:MaxRAMPercentage=75 to size the heap from the cgroup limit instead of the 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 get the same issue with another setting, because raising num_ctx dey increase 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 e threads. Once e use all of am, kernel go make am wait until the next period.
Na this contrast important. If container pass e memory limit, dem go kill am. If container pass e CPU limit, dem go throttle am and e go continue, but slower. So you fit set CPU limit aggressively without much risk, but memory limit need extra headroom.
cpu_shares na another 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. For idle machine, no one restrict dem. Use shares to rank services by importance, and use cpus when you need an actual ceiling. For example, e fit stop nightly transcode job from starving your web server.
FAQ
deploy.resources.limits fit 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>. E go print the limit for bytes, and e go 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 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, 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. If you set both for one service, e only make the file harder to read. Pick one, then 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. The kernel fit drop page cache when pressure dey, 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 the kernel no fit reclaim. High file value beside low anon value mean say container dey do disk input and output, not say e dey near 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. 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 target wey you must fill.