EthicsComputerCourses
Join training +91 90532 10052

Resource Limits: Memory, CPU & Processes

Docker & DevOps · Lesson 20 of 39 Intermediate 8 min read Updated

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:

Terminal
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 hog
Output
exit 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:

OptionMeaning
--memory 512mHard limit for RAM
--memory-swap 512mLimit 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 -1Unlimited swap (avoid on servers)
--memory-reservation 256mA 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.

Terminal
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.5 is a hard cap: at most one and a half cores' worth of CPU time, even when the server is idle.
  • --cpu-shares is 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,1 restricts 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:

Terminal
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:

Terminal
docker stats --no-stream invoice-pdf
Output
CONTAINER 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:

Terminal
docker update --memory 512m --memory-swap 512m --cpus 2 invoice-pdf

docker 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:

YAML
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: 65536

limits 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=75 to let the heap use more of it.
  • Node.js: set the heap size explicitly, for example NODE_OPTIONS=--max-old-space-size=384 in 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_children the 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 RestartCount and OOMKilled.
  • Forgetting swap. --memory 512m alone may allow another 512 MB of slow swap. Set --memory-swap to the same value.
  • Thinking CPU limits kill containers. They only slow them down; memory is the limit that kills.
  • Changing limits with docker update only. Write them into the Compose file as well.

Key points

  • --memory is a hard limit; going over it gets the process OOM-killed with exit code 137 and OOMKilled=true.
  • Set --memory-swap equal to --memory to disable swap; --memory-reservation is a soft limit used under memory pressure.
  • --cpus caps CPU time, --cpu-shares sets a relative weight under contention, --cpuset-cpus pins cores.
  • --pids-limit and --ulimit stop process explosions and set open-file limits.
  • Use docker stats to watch usage, docker update for live changes, and deploy.resources in 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
YAML
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: 768M
Terminal
docker 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?

2. What does --cpu-shares 512 do when the server's CPU is idle?

3. How do you stop a container from using any swap?

4. What does --pids-limit 200 protect against?

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