Resource Limits: Memory, CPU & Processes
On a shared VPS, one bad container can take everything. A report job with a memory leak eats all the RAM, the Linux kernel starts killing processes to survive, and it may pick your database or even the SSH server. A crypto-mining script dropped into a hacked container keeps every CPU core at 100 percent, and your real app slows to a crawl.
Resource limits prevent this. Docker uses the kernel's control groups (cgroups), which you met in Containers vs Virtual Machines, to cap how much memory, CPU and how many processes each container may use. This lesson covers every limit you need on a real server, how to see a limit being hit, and how to make your language runtime behave well inside one.
Memory limits and the OOM killer
--memory (short form -m) sets a hard memory limit. When the processes in the container try to use more, the kernel's OOM killer (out-of-memory killer) stops the biggest one. The smallest allowed value is 6m.
Try it with a Python one-liner that builds a 200 MB string inside a 64 MB container:
docker run --name hog --memory 64m --memory-swap 64m python:3.14-slim \
python -c "data = 'x' * (200 * 1024 * 1024)"
echo "exit code: $?"
docker inspect --format 'OOMKilled={{.State.OOMKilled}}' hog
docker rm hogexit code: 137 OOMKilled=true hog
Exit code 137 means the process was killed with SIGKILL, and OOMKilled=true confirms the memory limit was the reason. The program printed nothing: an OOM kill gives the app no chance to log an error, which is why it is easy to miss without checking docker inspect or docker events.
The memory options work together:
| Option | Meaning |
|---|---|
--memory 512m | Hard limit for RAM |
--memory-swap 512m | Limit for RAM plus swap; equal to --memory means no swap at all |
(no --memory-swap) | If the host has swap, the container may use as much swap as its memory limit, so 512m allows 1 GB in total |
--memory-swap -1 | Unlimited swap (avoid on servers) |
--memory-reservation 256m | A soft limit that Docker applies only when the host is short of memory |
Swap is much slower than RAM, so for web apps most teams set --memory-swap equal to --memory: a container that runs out of memory should fail fast and restart, not crawl.
CPU limits
CPU is different: a container that hits its CPU limit is slowed down, never killed.
docker run -d --name invoice-pdf --cpus 1.5 --cpu-shares 512 \
ghcr.io/aarav-shah/invoice-pdf:3.0.0
docker run -d --name hostel-api --cpuset-cpus 0,1 \
ghcr.io/aarav-shah/hostel-api:1.2.0--cpus 1.5is a hard cap: at most one and a half cores' worth of CPU time, even when the server is idle.--cpu-sharesis a relative weight; the default is 1024. It only matters when containers compete for CPU: under contention, a container with 512 gets about half the share of one with 1024. When the CPU is free, it can use as much as it likes.--cpuset-cpus 0,1restricts the container to cores 0 and 1 (counting starts at 0). It is useful for keeping a noisy batch job away from the cores the database uses.
Processes and open files
Two smaller limits protect the host from runaway programs:
docker run -d --name ticket-worker \
--pids-limit 200 \
--ulimit nofile=65536:65536 \
ghcr.io/aarav-shah/ticket-worker:2.1.0--pids-limit caps how many processes and threads can exist in the container. A bug that keeps starting new processes (or an attacker's "fork bomb") then hits the limit instead of freezing the whole server. --ulimit nofile=SOFT:HARD sets how many files and network connections each process may open, which busy proxies and databases sometimes need raised.
Watching usage and changing limits live
docker stats shows each container's usage against its limit:
docker stats --no-stream invoice-pdfCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 3f2a9c1b7d4e invoice-pdf 149.62% 211.4MiB / 256MiB 82.58% 1.2kB / 0B 0B / 0B 7
This sample assumes the container was started with --memory 256m as well. CPU above 100 percent means more than one core is in use; 149.62 percent is right at the --cpus 1.5 cap. Memory at 82 percent of the limit is a warning sign. You can raise limits on a running container without restarting it:
docker update --memory 512m --memory-swap 512m --cpus 2 invoice-pdfdocker update changes the live container only. Put the new values in your Compose file or run script too, or the old limits come back the next time the container is recreated.
Limits in Compose
Compose accepts limits under deploy.resources, the same structure Kubernetes-style tools and Swarm use:
services:
api:
image: ghcr.io/aarav-shah/hostel-api:1.2.0
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
pids: 200
reservations:
memory: 256M
ulimits:
nofile:
soft: 65536
hard: 65536limits are hard caps. reservations tell Docker how much the service needs; on a single host it works like --memory-reservation. You may also see the older service-level keys mem_limit, cpus and pids_limit in existing files; they still work, but do not mix both styles for the same setting.
Make the runtime respect the limit
A limit only helps if the program inside knows about it:
- Java has been container-aware for years: the JVM reads the cgroup memory limit. Its default maximum heap is a quarter of that limit, so set
-XX:MaxRAMPercentage=75to let the heap use more of it. - Node.js: set the heap size explicitly, for example
NODE_OPTIONS=--max-old-space-size=384in a 512 MB container, so garbage collection runs before the kernel steps in. Leave room for memory outside the heap. - Python web servers such as gunicorn start several worker processes, each with its own memory. Work out workers × memory per worker and keep it below the limit.
- PHP-FPM uses
pm.max_childrenthe same way.
Common mistakes
- No limits at all on a shared server. One leak can make the kernel kill unrelated processes, including the database.
- A limit far below normal use. The app is OOM-killed again and again, and the restart policy hides it. Check
RestartCountandOOMKilled. - Forgetting swap.
--memory 512malone may allow another 512 MB of slow swap. Set--memory-swapto the same value. - Thinking CPU limits kill containers. They only slow them down; memory is the limit that kills.
- Changing limits with
docker updateonly. Write them into the Compose file as well.
Key points
--memoryis a hard limit; going over it gets the process OOM-killed with exit code 137 andOOMKilled=true.- Set
--memory-swapequal to--memoryto disable swap;--memory-reservationis a soft limit used under memory pressure. --cpuscaps CPU time,--cpu-sharessets a relative weight under contention,--cpuset-cpuspins cores.--pids-limitand--ulimitstop process explosions and set open-file limits.- Use
docker statsto watch usage,docker updatefor live changes, anddeploy.resourcesin Compose; tune Java, Node.js, gunicorn or PHP-FPM to fit inside the limit.
Exercise
Kabir's Compose service results-worker runs a Node.js queue worker on a 4 GB VPS shared with MySQL. Give it a hard limit of 768 MB with no swap, at most half a CPU, at most 100 processes, and a Node.js heap sized to fit. Write the service definition. Then write the command that shows whether it was ever OOM-killed.
Show solution
services:
results-worker:
image: ghcr.io/kabir-mehta/results-worker:1.0.0
environment:
NODE_OPTIONS: "--max-old-space-size=576"
deploy:
resources:
limits:
cpus: "0.5"
memory: 768M
pids: 100
memswap_limit: 768Mdocker inspect --format 'OOMKilled={{.State.OOMKilled}} restarts={{.RestartCount}}' $(docker compose ps -q results-worker)memswap_limit is the Compose name for --memory-swap; setting it equal to the memory limit disables swap. The heap of 576 MB is 75 percent of the limit, leaving room for Node.js memory outside the heap. docker compose ps -q prints the container ID for docker inspect.
Quick Quiz
1. A container exits with code 137 and OOMKilled=true. What happened?
CPU limits slow containers; memory limits kill them.
2. What does --cpu-shares 512 do when the server's CPU is idle?
Use --cpus for a hard cap.
3. How do you stop a container from using any swap?
The swap value is RAM plus swap, so equal values leave no room for swap.
4. What does --pids-limit 200 protect against?
Process explosions hit the container's limit instead of the host's.
Learn Docker & DevOps with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
