sehaj/learn
Docker/03 Running containers

03.06 · with code · 11 min

CPU and memory limits

Give each container a budget so one runaway process can't take down everything else on the machine, and make sure your app knows about the budget.

The core idea

Without limits, a container can use all of the host's memory and CPU. One leaking process then slows or kills everything else on the server. Limits are set per container with docker run flags, and the kernel enforces them through cgroups. The two that matter most behave very differently: memory is a hard wall, CPU is a throttle.

Memory: a hard wall

❯terminal
$ docker run --name hog --memory 32m node:22-slim node -e 'const a=[];while(true)a.push(Buffer.alloc(1e6,1))'
$ docker inspect -f '{{.State.Status}} {{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' hog
exited 137 OOMKilled=true

At the limit, the kernel can't give out more memory, so its OOM killer ends a process in the container. There's no slowing down first.

FlagMeans
--memory 512mhard limit on RAM
--memory-swap 512mRAM + swap combined; set it equal to --memory to allow no swap
--memory-reservation 256ma soft target the kernel pushes towards when the host is short on memory

CPU: a throttle

❯terminal
$ docker run -d --name burner --cpus 0.5 node:22-slim node -e 'for(;;){}'
$ docker stats --no-stream burner --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
NAME CPU % MEM USAGE / LIMIT
burner 50.53% 7.484MiB / 7.653GiB

An infinite loop would happily use a whole core. With --cpus 0.5 it gets half of one, and is simply made to wait for the rest. Nothing is killed; the app just runs slower.

FlagMeans
--cpus 1.5at most one and a half cores' worth of CPU time
--cpu-shares 512relative weight when CPUs are contended (default 1024); no effect when the host is idle
--cpuset-cpus 0,1pin to specific cores

Note the memory column: with no --memory, the limit shown is everything Docker has, here the Docker Desktop VM's 7.65 GiB.

Processes: a fork-bomb guard

❯terminal
$ docker run --rm --pids-limit 20 alpine sh -c 'for i in $(seq 1 30); do sleep 5 & done; wait'
sh: can't fork: Resource temporarily unavailable

--pids-limit caps how many processes and threads a container can create.

Does your app know about the limit?

A limit only helps if the runtime inside respects it. Node sizes its heap from available memory, and recent versions read the cgroup limit:

❯terminal
$ docker run --rm node:22-slim node -e 'console.log(Math.round(require("v8").getHeapStatistics().heap_size_limit/1048576))'
2096
$ docker run --rm --memory 512m node:22-slim node -e 'console.log(Math.round(require("v8").getHeapStatistics().heap_size_limit/1048576), Math.round(process.constrainedMemory()/1048576))'
259 512

Under a 512 MB limit, Node 22 caps its heap at 259 MB, leaving room for buffers and native memory. Not every runtime does this: older Java (before 10) and older Node versions ignored the cgroup limit and sized themselves from the host's memory, then got OOM-killed. Check yours the same way before trusting it. If yours does, set it explicitly: NODE_OPTIONS=--max-old-space-size=384, Java's -XX:MaxRAMPercentage, or a worker count tied to --cpus.

Predict first

A container with --memory 256m --cpus 0.25 starts doing heavy work. Which is true?

Commit to an answer before you read on.
CheckpointBudget the notes API

Run the notes API with --memory 128m --cpus 0.5 --pids-limit 100 and check all three in docker stats --no-stream and docker inspect. Then use docker update --memory 256m --memory-swap 256m notes to raise the memory limit on the running container.

STUCK? ▸

docker stats shows no limit: you passed the flags after the image name, so they went to Node instead of Docker. Flags go before the image.

Recap

Interview questions

MidfreqMust-knowConcept

How do memory and CPU limits behave differently when a container exceeds them?

SeniorfreqGood to knowDebugging

A Java (or old Node) service keeps getting OOM-killed in a container even though its heap setting looks fine. Why?

Report a mistakeEdit this page