Containers share the host system's resources, and without proper controls, a single container can consume all available CPU, memory, or I/O capacity, starving other containers and destabilizing your application. Docker Compose provides comprehensive resource management capabilities, allowing you to set hard limits, soft reservations, and fine-grained controls over how containers access and consume system resources.
Understanding Resource Constraints
Resource constraints fall into two categories: hard limits and soft reservations. Hard limits represent absolute maximums that containers cannot exceed, while soft reservations indicate preferred resource allocations that the system attempts to honor but can exceed under certain conditions.
When you don't specify resource constraints, containers can use unlimited resources from the host. This might work well for development environments but creates unpredictable behavior in production where resource contention can cause performance degradation or system instability.
Basic Memory Limits
Memory is the most critical resource to constrain because out-of-memory conditions can cause containers or even the host system to crash. Set memory limits using the mem_limit option:
version: '3.8'
services:
web:
image: nginx:alpine
mem_limit: 512m
api:
image: node:18-alpine
mem_limit: 1g
database:
image: postgres:14
mem_limit: 2g
Memory limits can be specified in bytes or with suffixes:
- b: bytes
- k or kb: kilobytes
- m or mb: megabytes
- g or gb: gigabytes
When a container attempts to use more memory than its limit, the kernel's Out-Of-Memory (OOM) killer terminates processes within the container to free memory.
Memory Reservations
Memory reservations set soft limits that suggest preferred memory allocations without enforcing hard maximums:
version: '3.8'
services:
api:
image: node:18-alpine
mem_limit: 2g
mem_reservation: 1g
This configuration tells the system:
- Preferably allocate 1GB to this container
- Allow it to use up to 2GB if needed
- Under memory pressure, try to reclaim memory down to the 1GB reservation
Memory reservations work through the kernel's memory cgroups, which prioritize containers with higher reservations when distributing available memory.
Swap Memory Control
By default, containers can use swap space equal to their memory limit. Control swap usage with memswap_limit:
version: '3.8'
services:
database:
image: postgres:14
mem_limit: 2g
memswap_limit: 3g
The memswap_limit represents the total of memory plus swap. In this example:
- Physical memory limit: 2GB
- Swap space available: 1GB (3GB total - 2GB memory)
Setting memswap_limit equal to mem_limit effectively disables swap for that container:
services:
cache:
image: redis:alpine
mem_limit: 512m
memswap_limit: 512m # No swap allowed
Disabling swap is recommended for latency-sensitive services like caches and databases where swap-induced delays are unacceptable.
CPU Shares for Proportional Allocation
CPU shares implement relative CPU allocation among containers. The default share value is 1024, and containers receive CPU time proportional to their share value:
version: '3.8'
services:
high-priority:
image: myapp:latest
cpu_shares: 2048 # Gets 2x CPU time
normal-priority:
image: worker:latest
cpu_shares: 1024 # Gets 1x CPU time
low-priority:
image: batch:latest
cpu_shares: 512 # Gets 0.5x CPU time
If all three containers are CPU-bound simultaneously:
- high-priority gets approximately 57% of CPU time (2048/3584)
- normal-priority gets approximately 29% (1024/3584)
- low-priority gets approximately 14% (512/3584)
CPU shares only matter under contention. If high-priority is idle, other containers can use its allocated CPU time.
CPU Quota and Period
CPU quota provides hard limits on CPU usage measured in microseconds per period:
version: '3.8'
services:
api:
image: node:18-alpine
cpu_quota: 50000
cpu_period: 100000
This configuration limits the container to 50,000 microseconds of CPU time per 100,000 microsecond period, effectively capping it at 50% of one CPU core.
The relationship is:
CPU percentage = (cpu_quota / cpu_period) * 100
To limit a container to 1.5 CPU cores:
services:
compute:
image: python:3.9
cpu_quota: 150000
cpu_period: 100000 # 150,000/100,000 = 1.5 cores
CPU Count Limits
The cpus option provides a simpler way to set CPU limits:
version: '3.8'
services:
web:
image: nginx:alpine
cpus: 0.5 # Half a CPU core
api:
image: node:18-alpine
cpus: 2.0 # Two full CPU cores
worker:
image: python:3.9
cpus: 1.5 # One and a half CPU cores
The cpus value can be a decimal and represents the number of CPU cores available to the container. This is a more intuitive alternative to manually calculating cpu_quota and cpu_period values.
CPU Affinity and Pinning
CPU affinity controls which specific CPU cores a container can use:
version: '3.8'
services:
dedicated-task:
image: compute:latest
cpuset_cpus: "0,1" # Only use cores 0 and 1
another-task:
image: compute:latest
cpuset_cpus: "2,3" # Only use cores 2 and 3
CPU pinning isolates workloads and can improve cache locality and reduce context switching. Range notation is also supported:
services:
task:
image: compute:latest
cpuset_cpus: "0-3,6-7" # Use cores 0,1,2,3,6,7
Memory Node Affinity (NUMA)
On NUMA (Non-Uniform Memory Access) systems, specify which memory nodes containers should use:
version: '3.8'
services:
database:
image: postgres:14
cpuset_cpus: "0-3"
cpuset_mems: "0" # Use memory from NUMA node 0
This ensures the container uses memory local to its assigned CPU cores, reducing memory access latency on multi-socket systems.
Block I/O Weight
Control relative block I/O bandwidth allocation using weights:
version: '3.8'
services:
database:
image: postgres:14
blkio_weight: 1000 # Higher priority for disk I/O
logs:
image: fluentd:latest
blkio_weight: 500 # Normal priority
backup:
image: backup:latest
blkio_weight: 100 # Lower priority
Weights range from 10 to 1000 (default is 500). Like CPU shares, I/O weight only matters under contention. When multiple containers compete for disk I/O, bandwidth is distributed proportionally to their weights.
Device Read/Write Rate Limits
Set absolute limits on device read and write rates:
version: '3.8'
services:
database:
image: postgres:14
device_read_bps:
- path: /dev/sda
rate: 50mb
device_write_bps:
- path: /dev/sda
rate: 30mb
This configuration limits the database container to:
- 50 MB/s read rate from /dev/sda
- 30 MB/s write rate to /dev/sda
Rates can use the same suffixes as memory limits: kb, mb, gb.
Device I/O Operations Limits
Limit the number of I/O operations per second (IOPS):
version: '3.8'
services:
application:
image: myapp:latest
device_read_iops:
- path: /dev/sda
rate: 1000
device_write_iops:
- path: /dev/sda
rate: 500
This caps the container at:
- 1000 read operations per second
- 500 write operations per second
IOPS limits are particularly useful for preventing noisy neighbors in environments with shared storage.
PID Limits
Limit the number of processes a container can create:
version: '3.8'
services:
web:
image: nginx:alpine
pids_limit: 100
api:
image: node:18-alpine
pids_limit: 200
PID limits prevent fork bombs and runaway process creation that could exhaust system resources. Setting appropriate limits depends on your application's normal process count.
Combining Resource Constraints
Real-world services typically need multiple resource constraints:
version: '3.8'
services:
database:
image: postgres:14
mem_limit: 4g
mem_reservation: 2g
memswap_limit: 4g
cpus: 2.0
cpu_shares: 2048
blkio_weight: 1000
pids_limit: 500
cache:
image: redis:alpine
mem_limit: 1g
mem_reservation: 512m
memswap_limit: 1g
cpus: 0.5
cpu_shares: 1024
pids_limit: 100
worker:
image: python:3.9
mem_limit: 2g
mem_reservation: 1g
cpus: 1.5
cpu_shares: 1024
blkio_weight: 500
pids_limit: 200
This configuration creates a clear resource hierarchy where the database receives priority for CPU, memory, and I/O operations.
OOM Kill Disable
Prevent the OOM killer from terminating specific containers:
version: '3.8'
services:
critical-service:
image: myapp:latest
mem_limit: 2g
oom_kill_disable: true
Warning: Disabling OOM kill is dangerous. If the container exceeds its memory limit and the OOM killer is disabled, it can cause system-wide instability. Only use this for critical services where you've carefully calculated memory requirements and have monitoring in place.
OOM Score Adjustment
Influence which containers the OOM killer targets first:
version: '3.8'
services:
critical-database:
image: postgres:14
mem_limit: 4g
oom_score_adj: -500 # Less likely to be killed
cache:
image: redis:alpine
mem_limit: 1g
oom_score_adj: 0 # Normal likelihood
batch-processor:
image: worker:latest
mem_limit: 2g
oom_score_adj: 500 # More likely to be killed
OOM score values range from -1000 to 1000:
- Negative values decrease likelihood of being killed
- Positive values increase likelihood of being killed
- 0 is the default
This allows you to define which services are expendable during memory pressure situations.
Restart Policies for Resource Failures
Configure how containers restart when they're killed due to resource constraints:
version: '3.8'
services:
web:
image: nginx:alpine
mem_limit: 512m
restart: on-failure:3
api:
image: node:18-alpine
mem_limit: 1g
restart: unless-stopped
worker:
image: python:3.9
mem_limit: 2g
restart: always
Restart policy options:
- no: Never restart (default)
- always: Always restart
- on-failure: Restart only on non-zero exit codes
- on-failure:n: Restart up to n times on failure
- unless-stopped: Always restart unless explicitly stopped
When a container is OOM-killed, it exits with a non-zero status, triggering on-failure restart policies.
CPU Realtime Runtime and Period
For real-time workloads, configure CPU realtime scheduler access:
version: '3.8'
services:
realtime-app:
image: realtime:latest
cpu_rt_runtime: 50000
cpu_rt_period: 100000
This allocates 50,000 microseconds of realtime CPU scheduling per 100,000 microsecond period. Realtime scheduling requires special kernel capabilities and is used for latency-sensitive applications that need guaranteed CPU time.
Privileged Containers and Resource Limits
Privileged containers can bypass certain resource constraints, but you can still enforce limits:
version: '3.8'
services:
privileged-service:
image: system-tool:latest
privileged: true
mem_limit: 2g
cpus: 1.0
Even with privileged: true, memory and CPU limits remain enforced, preventing the container from consuming all host resources.
Container Resource Isolation Modes
Understanding how resource isolation works at the kernel level helps optimize configurations. Docker uses Linux cgroups (control groups) to enforce resource limits.
Memory cgroups track and limit memory usage:
- RSS (Resident Set Size): Physical memory
- Cache: File cache memory
- Swap: Swapped memory
CPU cgroups control CPU time:
- CFS (Completely Fair Scheduler): Default scheduler for normal processes
- RT (Real-time): For real-time processes requiring guaranteed CPU time
Block I/O cgroups manage disk I/O:
- Throttling: Hard limits on throughput
- Weight: Proportional bandwidth allocation
Resource Limit Inheritance
When using extends or including external configurations, resource limits follow specific inheritance rules:
version: '3.8'
services:
base-service:
image: baseapp:latest
mem_limit: 1g
cpus: 1.0
extended-service:
extends:
service: base-service
mem_limit: 2g # Overrides base memory limit
# Inherits cpus: 1.0
Resource limits specified in the extending service override inherited values, while unspecified limits are inherited from the base service.
Monitoring Resource Usage
View resource usage for running containers:
docker stats
This displays real-time CPU, memory, network, and disk I/O usage for all containers. For specific containers:
docker stats container_name
Understanding actual resource consumption helps tune limit values. Set limits with headroom above normal usage but below acceptable maximums.
Resource Guarantees vs Limits
Some resource settings guarantee minimum resources while others cap maximum usage:
Guarantees (Soft Limits):
- mem_reservation: Guaranteed memory availability
- cpu_shares: Guaranteed relative CPU time under contention
Caps (Hard Limits):
- mem_limit: Maximum memory usage
- cpus: Maximum CPU cores
- cpu_quota: Maximum CPU time per period
Effective resource management often requires both guarantees and caps:
services:
api:
image: node:18-alpine
mem_reservation: 512m # Guaranteed minimum
mem_limit: 2g # Maximum allowed
cpu_shares: 1024 # Guaranteed relative share
cpus: 2.0 # Maximum cores
Calculating Resource Requirements
Determine appropriate resource limits through testing and monitoring:
- Run without limits initially to establish baseline resource usage
- Monitor peak usage under realistic load conditions
- Add safety margins: Set limits 20-50% above peak usage
- Test limit enforcement: Verify containers handle hitting limits gracefully
- Adjust based on observations: Fine-tune limits as usage patterns emerge
Example progression:
# Phase 1: Unrestricted baseline
services:
api:
image: node:18-alpine
# No limits - monitor actual usage
# Phase 2: Conservative limits (observed peak + 50%)
services:
api:
image: node:18-alpine
mem_limit: 1.5g # Observed 1g peak
cpus: 2.0 # Observed 1.5 core peak
# Phase 3: Optimized limits (observed peak + 20%)
services:
api:
image: node:18-alpine
mem_limit: 1.2g
cpus: 1.8
Resource Contention Scenarios
Understanding how containers behave under resource contention helps set appropriate limits:
Memory contention:
- Containers reaching mem_limit face OOM kills
- Containers below mem_reservation are protected from reclaim
- Swap usage increases latency significantly
CPU contention:
- Containers share CPU based on cpu_shares ratios
- Containers hitting cpu_quota are throttled
- CPU-bound containers starve I/O operations
I/O contention:
- Containers compete based on blkio_weight values
- Containers hitting device_read/write_bps limits queue operations
- High IOPS can bottleneck even with bandwidth available
Layered Resource Allocation Strategy
Implement tiered resource allocation for different service priorities:
version: '3.8'
services:
# Tier 1: Critical services (highest resources)
database:
image: postgres:14
mem_limit: 4g
mem_reservation: 3g
cpus: 3.0
cpu_shares: 3072
blkio_weight: 1000
oom_score_adj: -500
# Tier 2: Important services (medium resources)
api:
image: node:18-alpine
mem_limit: 2g
mem_reservation: 1g
cpus: 2.0
cpu_shares: 2048
blkio_weight: 750
oom_score_adj: 0
# Tier 3: Background services (lower resources)
worker:
image: python:3.9
mem_limit: 1g
mem_reservation: 512m
cpus: 1.0
cpu_shares: 1024
blkio_weight: 500
oom_score_adj: 300
# Tier 4: Best-effort services (minimal guaranteed resources)
monitoring:
image: prometheus:latest
mem_limit: 512m
cpus: 0.5
cpu_shares: 512
blkio_weight: 100
oom_score_adj: 500
This hierarchy ensures critical services receive priority during resource contention while background services can still function when resources are available.
Dynamic Resource Adjustment
While Docker Compose doesn't support runtime resource updates directly, you can structure services to enable adjustment:
version: '3.8'
services:
app-light:
image: myapp:latest
mem_limit: 1g
cpus: 1.0
app-heavy:
image: myapp:latest
mem_limit: 4g
cpus: 4.0
Switch between configurations by starting different service variants:
docker-compose up app-light # Light resource allocation docker-compose up app-heavy # Heavy resource allocation
Resource Limits for Multi-Container Services
When running multiple containers for the same service, set per-container limits:
version: '3.8'
services:
api:
image: node:18-alpine
mem_limit: 1g
cpus: 1.0
Scaling to three containers:
docker-compose up --scale api=3
Creates three containers, each with:
- 1GB memory limit
- 1.0 CPU core limit
Total system impact: 3GB memory and 3 CPU cores maximum.
Handling Memory-Intensive Startup
Some applications require more memory during startup than during normal operation:
version: '3.8'
services:
java-app:
image: java-app:latest
mem_limit: 4g # Higher limit for startup
mem_reservation: 2g # Expected runtime usage
environment:
- JVM_OPTS=-Xmx3g # JVM heap limit below mem_limit
This configuration allows the JVM to allocate a 3GB heap while keeping total container memory usage under 4GB, preventing OOM kills during startup while indicating the expected 2GB runtime usage through the reservation.
Resource Limits for Build Containers
When building images within Compose, apply resource limits to build containers:
version: '3.8'
services:
app:
build:
context: .
shm_size: 2gb
mem_limit: 2g
cpus: 2.0
The shm_size parameter controls shared memory size, which affects /dev/shm within the container. Some applications, particularly browsers and databases, require larger shared memory allocations.
Kernel Memory Limits
Control kernel memory usage separately from user space memory:
version: '3.8'
services:
network-intensive:
image: myapp:latest
mem_limit: 2g
kernel_memory: 256m
Kernel memory includes network buffers, inode cache, and other kernel structures. Limiting kernel memory prevents containers from consuming excessive kernel resources through network operations or file system activity.
Resource Limit Best Practices
Start conservative, then optimize: Begin with generous limits and tighten based on actual usage patterns.
Monitor continuously: Resource requirements change as applications evolve. Regular monitoring identifies when limits need adjustment.
Test failure scenarios: Verify your application handles resource exhaustion gracefully. Intentionally trigger limit violations during testing.
Document limit rationale: Comment your resource limits explaining the reasoning:
services:
api:
image: node:18-alpine
mem_limit: 2g # Peak observed: 1.5g during batch processing
cpus: 1.5 # Average: 0.8, peak: 1.2 during request spikes
blkio_weight: 750 # Moderate I/O priority (logs + db queries)
Account for multiple instances: When scaling horizontally, ensure total resource usage across all instances fits within system capacity.
Balance limits with availability: Overly aggressive limits cause frequent OOM kills and restarts, harming availability. Find the balance between resource protection and service reliability.
Consider cgroup v2 features: Modern kernels support cgroup v2, which provides enhanced resource management capabilities. Ensure your Docker version supports the features you're using.
Use memory reservations strategically: Set reservations for guaranteed performance under contention while allowing bursts through higher limits.
Coordinate CPU and I/O limits: CPU-intensive applications may need lower I/O weights, while I/O-bound applications benefit from higher I/O weights even with lower CPU allocations.
Test on representative hardware: Resource requirements vary significantly based on underlying hardware. Test limits on systems matching your deployment environment.
Docker Compose resource management provides fine-grained control over container resource consumption, enabling stable, predictable multi-container environments. By understanding and properly configuring memory limits, CPU allocations, I/O constraints, and process limits, you can optimize resource utilization while preventing individual containers from impacting overall system stability.