Docker Resource Limits: CPU, Memory, and I/O Constraints Explained
Without resource limits, a single misbehaving container can consume all available memory and crash the host, starve other containers of CPU time, or saturate disk I/O to the point where the system becomes unresponsive. Resource limits are not optional in production โ they are the guardrails that keep your infrastructure stable when things go wrong.
Docker uses Linux control groups (cgroups) to enforce resource constraints on containers. This guide covers every dimension of resource limiting: memory, CPU, block I/O, and the underlying cgroups mechanisms that make it all work.
Memory Limits
Memory is the most critical resource to limit because the consequences of overconsumption are severe. When a container exhausts available system memory, the Linux OOM (Out-Of-Memory) killer starts terminating processes, potentially killing unrelated containers or even critical system processes.
Setting Memory Limits
# Hard memory limit of 512MB
docker run --memory=512m myapp
# Memory limit with swap
docker run --memory=512m --memory-swap=1g myapp
# Memory limit with no swap (memory-swap equals memory)
docker run --memory=512m --memory-swap=512m myapp
# Soft limit (memory reservation) โ the scheduler will try to
# keep the container at this level but allows bursting
docker run --memory=1g --memory-reservation=512m myapp
Understanding the relationship between --memory and --memory-swap:
| Configuration | RAM Available | Swap Available | Behavior |
|---|---|---|---|
--memory=512m (no swap flag) |
512 MB | 512 MB | Total 1 GB usable (default: swap = memory) |
--memory=512m --memory-swap=1g |
512 MB | 512 MB | Total 1 GB usable (swap = memory-swap minus memory) |
--memory=512m --memory-swap=512m |
512 MB | 0 MB | No swap usage allowed |
--memory=512m --memory-swap=-1 |
512 MB | Unlimited | Can use all available host swap |
The OOM Killer
When a container exceeds its memory limit, the Linux kernel's OOM killer terminates the process inside the container. Docker then records this as an OOMKilled event:
# Check if a container was killed due to OOM
docker inspect --format='{{.State.OOMKilled}}' mycontainer
# Returns: true
# View container exit details
docker inspect --format='{{json .State}}' mycontainer | jq .
# "ExitCode": 137 indicates SIGKILL from OOM
You can adjust the OOM killer priority for a container. A lower score makes the container less likely to be killed when the host runs low on memory:
# Protect this container from OOM killer (-1000 to 1000)
docker run --oom-score-adj=-500 --memory=1g critical-app
# Disable OOM killer entirely (dangerous โ can freeze the host)
docker run --oom-kill-disable --memory=1g myapp
Warning: Never use
--oom-kill-disablewithout setting a memory limit. If the container can allocate unlimited memory and the OOM killer is disabled, the entire host can become unresponsive.
Memory Limits in Docker Compose
services:
api:
image: myapp:latest
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
# Legacy syntax (still works but deploy is preferred)
worker:
image: myworker:latest
mem_limit: 256m
mem_reservation: 128m
memswap_limit: 512m
CPU Limits
CPU constraints work differently than memory constraints. Rather than hard caps that trigger a kill, CPU limits throttle the container's access to processor time. There are two primary mechanisms: CPU shares (relative weighting) and CPU quotas (absolute limits).
CPU Shares (Relative Weight)
CPU shares define the relative amount of CPU time a container receives when there is contention. The default value is 1024:
# Give this container double the default CPU priority
docker run --cpu-shares=2048 high-priority-app
# Give this container half the default CPU priority
docker run --cpu-shares=512 background-worker
CPU shares only matter when containers are competing for CPU. If a container is the only one using the CPU, it gets full access regardless of its share value. This makes shares a soft limit.
CPU Quotas (Hard Limit)
The --cpus flag sets a hard limit on how many CPU cores a container can use:
# Limit to 1.5 CPU cores
docker run --cpus=1.5 myapp
# Limit to exactly 1 CPU core
docker run --cpus=1 myapp
# Limit to 0.5 of a CPU core (50% of one core)
docker run --cpus=0.5 myapp
Under the hood, --cpus=1.5 translates to --cpu-period=100000 --cpu-quota=150000. The container gets 150,000 microseconds of CPU time every 100,000 microsecond period. You can set these values directly for more precise control:
# Equivalent to --cpus=0.25
docker run --cpu-period=100000 --cpu-quota=25000 myapp
CPU Pinning
For latency-sensitive workloads, you can pin a container to specific CPU cores:
# Run on cores 0 and 1 only
docker run --cpuset-cpus="0,1" myapp
# Run on cores 0 through 3
docker run --cpuset-cpus="0-3" myapp
# Combine with CPU limit
docker run --cpuset-cpus="0,1" --cpus=1.5 myapp
CPU pinning is particularly useful for NUMA-aware applications or when you need predictable latency without interference from other workloads.
CPU Limits in Docker Compose
services:
api:
image: myapp:latest
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
worker:
image: myworker:latest
cpuset: "0,1"
cpu_shares: 512
Block I/O Limits
Disk I/O can be a significant bottleneck, especially when multiple containers share the same storage device. Docker provides I/O throttling to prevent any single container from monopolizing disk bandwidth.
# Limit read speed to 10 MB/s on a specific device
docker run --device-read-bps /dev/sda:10mb myapp
# Limit write speed to 5 MB/s
docker run --device-write-bps /dev/sda:5mb myapp
# Limit read IOPS to 1000 operations per second
docker run --device-read-iops /dev/sda:1000 myapp
# Limit write IOPS to 500 operations per second
docker run --device-write-iops /dev/sda:500 myapp
# Set relative I/O weight (10-1000, default 500)
docker run --blkio-weight 300 background-task
Tip: Block I/O limits require the
blkiocgroup controller. On systems using cgroups v2, the I/O controller must be enabled. Check with:cat /sys/fs/cgroup/cgroup.controllers
Understanding Cgroups v1 vs v2
Docker's resource limits are implemented through Linux control groups. Modern distributions are migrating to cgroups v2, which changes how some resource controls work.
# Check which cgroup version your system uses
stat -fc %T /sys/fs/cgroup/
# "cgroup2fs" = cgroups v2
# "tmpfs" = cgroups v1 (or hybrid)
Key differences between cgroups v1 and v2:
| Feature | cgroups v1 | cgroups v2 |
|---|---|---|
| Hierarchy | Multiple hierarchies | Single unified hierarchy |
| Memory accounting | Separate memory and memsw | Unified memory tracking |
| I/O controller | blkio (limited) | io (supports buffered I/O) |
| CPU controller | cpu, cpuacct (separate) | cpu (unified) |
| Pressure stall info | Not available | PSI metrics available |
Docker Engine 20.10+ supports cgroups v2. If you are running a recent Linux distribution (Ubuntu 22.04+, Fedora 31+, Debian 11+), you are likely already on cgroups v2. Docker detects the cgroup version automatically and adjusts its behavior.
Real-World Sizing Guidelines
Setting appropriate limits requires understanding your application's actual resource usage. Here are starting points for common workloads:
| Workload Type | Memory Limit | CPU Limit | Notes |
|---|---|---|---|
| Static file server (nginx) | 128-256 MB | 0.25-0.5 | Primarily needs memory for connections |
| Node.js API | 256-512 MB | 0.5-1.0 | Single-threaded; limit CPU to 1 unless using workers |
| Python/Django API | 256-512 MB | 0.5-1.0 | Per-worker process; multiply by worker count |
| Java/Spring Boot | 512 MB-2 GB | 1.0-2.0 | Set -Xmx to ~75% of container memory limit |
| PostgreSQL | 1-4 GB | 1.0-4.0 | shared_buffers = 25% of memory limit |
| Redis (cache) | 256 MB-1 GB | 0.5-1.0 | Set maxmemory to 75% of container limit |
| Background worker | 128-512 MB | 0.25-0.5 | Depends heavily on task type |
The Sizing Workflow
Don't guess at resource limits. Measure first, then set limits with headroom:
# Step 1: Run without limits and observe actual usage
docker stats mycontainer
# Output shows real-time resource consumption:
# CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
# mycontainer 2.34% 187.4MiB / 15.5GiB 1.18% 1.2kB/0B 0B/0B
# Step 2: Check peak memory usage over time
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}" mycontainer
# Step 3: Set limits at 1.5-2x the observed peak
docker run --memory=384m --cpus=0.5 myapp
Tip: usulnet's dashboard provides real-time resource monitoring across all your containers. Instead of running
docker statson individual hosts, you can view memory and CPU usage trends from a single interface, making it much easier to set appropriate limits based on actual consumption patterns.
Java Applications: A Special Case
Java applications require special attention because the JVM has its own memory management. Before Java 10, the JVM did not recognize container memory limits and would try to allocate based on host memory, frequently leading to OOM kills.
# Modern Java (10+) is container-aware by default
docker run --memory=1g myapp-java
# Set JVM heap to 75% of container limit
docker run --memory=1g -e JAVA_OPTS="-Xmx768m -Xms384m" myapp-java
# For older Java (8u191+), enable container awareness explicitly
docker run --memory=1g \
-e JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" \
myapp-java
Remember that JVM memory includes more than just the heap. Account for metaspace, thread stacks, native memory, and direct buffers. A good rule of thumb: set the container memory limit to at least 1.5x your max heap size.
Monitoring Resource Usage
Once limits are in place, continuous monitoring ensures they remain appropriate as workloads change.
Docker Stats
# Real-time stats for all containers
docker stats
# Stats for specific containers, formatted
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.BlockIO}}"
# One-shot stats (good for scripting)
docker stats --no-stream --format '{{.Name}},{{.CPUPerc}},{{.MemUsage}}' > stats.csv
Checking Cgroup Files Directly
For detailed information, you can inspect the cgroup files that Docker creates:
# Find the container's cgroup path
CGROUP_PATH=$(docker inspect --format='{{.HostConfig.CgroupParent}}' mycontainer)
# cgroups v2: Memory current usage
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format='{{.Id}}' mycontainer).scope/memory.current
# cgroups v2: Memory limit
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format='{{.Id}}' mycontainer).scope/memory.max
# cgroups v2: CPU stats
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format='{{.Id}}' mycontainer).scope/cpu.stat
Setting Up Alerts
Combine resource monitoring with alerting to catch problems before they impact users:
# Example: Alert when container uses more than 80% of memory limit
docker stats --no-stream --format '{{.Name}} {{.MemPerc}}' | while read name pct; do
pct_num=$(echo "$pct" | tr -d '%')
if (( $(echo "$pct_num > 80" | bc -l) )); then
echo "WARNING: $name memory usage at $pct"
fi
done
Common Pitfalls
Not Setting Any Limits
The most common mistake is deploying containers without resource limits. In development this is fine, but in production, a single memory leak can take down every container on the host. Always set at least memory limits for production workloads.
Setting Limits Too Low
Overly aggressive limits cause constant OOM kills and CPU throttling, degrading application performance. If your container is frequently restarting with exit code 137, your memory limit is too low. If response times spike periodically, check for CPU throttling.
Ignoring Swap
By default, Docker allows containers to use as much swap as the memory limit. This can make a memory-constrained container appear to work while actually performing terribly due to swap thrashing. For latency-sensitive applications, disable swap: --memory-swap equal to --memory.
Forgetting Child Processes
Resource limits apply to all processes in the container's cgroup, including child processes. A container running nginx with 10 worker processes needs memory for all workers combined, not just the master process.
Resource limits are one of the most important aspects of running containers in production. Start by measuring actual usage, set limits with appropriate headroom, and monitor continuously. Tools like usulnet make this process significantly easier by providing real-time visibility into resource consumption across your entire container fleet.