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 --memory means 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=384 for 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.

  • Docker
  • Performance
  • Containers