Back to BlogDocker Container · Docker compose · docker

Resource Limits and Container Scheduling in Docker Compose

2025-12-26

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:

  1. Run without limits initially to establish baseline resource usage
  2. Monitor peak usage under realistic load conditions
  3. Add safety margins: Set limits 20-50% above peak usage
  4. Test limit enforcement: Verify containers handle hitting limits gracefully
  5. 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.

We use cookies to improve your experience and analyse site traffic. See our Privacy Policy.