How to Limit a Docker Container's CPU and Memory (Stop One App Starving the Rest)

Stop one container starving the rest: Memory and CPU limits in three lines.

Without limits, a container can use every byte of RAM and every core on the server. One runaway transcode in Jellyfin, one photo-indexing job in Immich, one Minecraft chunk-generation spree, and SSH stops responding and Pi-hole drops queries. A few lines in each compose file keep the heavy apps in their lane.

Quick reference

TaskWhat
Live usagedocker stats
Memory capmem_limit: 2g
CPU capcpus: "1.5"
Lower priority instead of a capcpu_shares: 512
Pin to corescpuset: "2,3"
What happens at the memory limitthe container's biggest process is killed (OOM)

1. See what's using what

docker stats

A live table: CPU %, memory used and limit, network, disk I/O per container. Leave it running while you do something heavy to see which container reacts. Ctrl + C to quit; docker stats --no-stream for a snapshot.

2. Memory limits

In the service's compose block:

services:
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:release
mem_limit: 3g
memswap_limit: 3g

mem_limit is the ceiling. memswap_limit equal to it means "no swap either" — otherwise a container at its limit starts swapping and slows the whole server. When a container hits the limit, the kernel kills its largest process; Docker restarts it (restart: unless-stopped) and you'll see "OOMKilled: true" in docker inspect.

Sensible starting points: Pi-hole 256m, Uptime Kuma 256m, Vaultwarden 256m, Nextcloud 1g, Jellyfin 2g, Immich ML 3g, Minecraft 1 GB more than its MEMORY setting.

3. CPU limits

cpus: "2"

The container gets at most two cores' worth of time, spread across any cores. Good for transcoding and indexing jobs that would otherwise take everything.

Often better than a hard cap is a lower priority:

cpu_shares: 256

Default is 1024. A container at 256 still uses idle CPU freely, but when there's contention it yields to the others. Give Minecraft and Jellyfin 512 and everything important stays snappy.

Pin to specific cores if you want a job to never touch the ones running your network services:

cpuset: "2,3"

4. Apply

docker compose up -d
docker stats

The memory column now shows used / limit.

5. Deploy-style syntax

You'll see this form in some guides:

deploy:
resources:
limits:
cpus: "2"
memory: 2g

It works with recent Docker Compose and is equivalent. Pick one style and stick to it.

6. The host's own protection

Even with limits, keep some headroom for the OS: on an 8 GB server, don't hand out 8 GB of limits. And add swap (tutorial 21) so an accidental spike degrades gracefully rather than killing something random.

Troubleshooting

Container restarts in a loop after adding a limit. The limit is below what it genuinely needs. docker inspect name | grep -i oom confirms; raise it.

Limits seem ignored. Old compose file version syntax, or the cgroup driver. docker info | grep -i cgroup should show cgroupfs or systemd with version 2. docker stats is the proof either way.

docker stats shows memory far above what the app reports. It includes file cache attributed to the container; it's reclaimable. Watch for growth over days rather than the absolute number.

Jellyfin transcodes stutter with a CPU limit. Hardware transcoding (tutorial 42) moves the work to the GPU, where limits don't apply.

Wrapping up

docker stats to see, mem_limit and cpus or cpu_shares to control, up -d to apply. Cap the known hogs, lower the priority of the games and media, and leave the small important services unlimited. The server stops having bad days because one container had a busy one.

Call us