By default a container can use all the CPU and memory of the host. On a VPS running several services, one runaway container — a memory leak, a heavy import, a traffic spike — can starve everything else, including SSH. Resource limits keep each container in its lane.
Memory limits
docker run -d --name app --memory 512m --memory-swap 512m myapp
--memory(-m) — hard limit. If the container tries to exceed it, the kernel’s OOM killer kills a process inside the container (usually the main one), and the container exits with code 137.--memory-swap— total of memory + swap. Setting it equal to--memorymeans no swap for the container — predictable behaviour. If omitted, the container may use swap up to the same amount again (when the host has swap).--memory-reservation 256m— a soft limit: under host memory pressure, Docker tries to shrink the container toward this value.
Check whether a container was OOM-killed:
docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' app
CPU limits
docker run -d --name worker --cpus 1.5 myworker
--cpus 1.5— at most 1.5 CPUs’ worth of time (spread over any cores).--cpuset-cpus 0,1— only run on specific cores.--cpu-shares 512— relative weight when CPUs are contended (default 1024); no effect when CPU is idle.
Unlike memory, exceeding a CPU limit doesn’t kill anything — the container is throttled and runs slower.
In Docker Compose
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
memory: 256M
Docker Compose v2 applies deploy.resources limits for regular (non-Swarm) deployments. The older service-level keys (mem_limit, cpus) also work but are legacy.
Change limits on a running container
docker update --memory 1g --memory-swap 1g --cpus 2 app
No restart needed. With Compose, update the file and run docker compose up -d so the change persists.
Watch actual usage
docker stats
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}'
MemUsage shows usage against the limit. Size limits from real data: watch a service under normal and peak load, then set the limit with headroom (often 1.5–2× typical usage).
Language runtimes and limits
Runtimes must know about the container limit, or they size themselves for the whole host:
- Java (JDK 10+) detects container limits automatically; tune with
-XX:MaxRAMPercentage=75. - Node.js — the V8 heap has its own limit; set
NODE_OPTIONS=--max-old-space-size=384for a 512 MB container to leave room for non-heap memory. - PHP-FPM, Gunicorn, Puma — set worker counts to fit the memory limit (workers × memory per worker).
- Databases — configure buffer sizes (
innodb_buffer_pool_size,shared_buffers) below the container limit, or they’ll be OOM-killed under load.
Protect the host
Leave memory for the host OS, SSH and Docker itself. On a 4 GB VPS, keep the sum of container limits around 3–3.3 GB. Adding host swap gives a buffer against sudden spikes — see swap file vs partition.
A container without limits that leaks memory can trigger the host’s OOM killer, which may pick any process — including your database. Limits contain the damage to the misbehaving container.
Other limits worth knowing
--pids-limit 200 # max processes (stops fork bombs)
--ulimit nofile=65536:65536
--storage-opt size=10G # writable layer size (some storage drivers only)
--device-read-bps /dev/vda:50mb
Quick reference
| Goal | docker run | Compose |
|---|---|---|
| Memory cap | --memory 512m |
deploy.resources.limits.memory: 512M |
| No swap | --memory-swap 512m |
memswap_limit: 512M |
| CPU cap | --cpus 1.5 |
deploy.resources.limits.cpus: "1.5" |
| Soft memory | --memory-reservation 256m |
deploy.resources.reservations.memory |
| Live change | docker update … |
edit + up -d |
Related: Docker Compose tutorial and how much RAM your VPS needs.