← Back to DevBytes

Docker Resource Limits: Production Guide

What Are Docker Resource Limits?

Docker resource limits are constraints you place on the amount of system resources—CPU, memory, disk I/O, and process count—that a container can consume. By default, a Docker container has unrestricted access to the host machine's resources. It can use as much CPU time as it needs, allocate as much memory as the kernel will give it, and spawn as many processes as the system allows. Resource limits change this behavior by defining a ceiling on what each container is allowed to use.

These limits are configured at container runtime through various flags passed to the docker run command or set declaratively in Docker Compose files. They leverage Linux kernel features such as cgroups (control groups) to enforce the boundaries. Docker's resource limiting mechanism is not merely a suggestion—the kernel actively throttles or kills processes that exceed the defined limits.

Key Resources You Can Limit

Why Resource Limits Matter in Production

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In development environments, a single developer might run a handful of containers on a laptop. Resource contention is rare and easily resolved by shutting down non-critical services. In production, however, dozens or hundreds of containers often share a cluster of hosts. Without resource limits, a single misbehaving container can degrade or completely crash every other service on the same node.

The "Noisy Neighbor" Problem

The most compelling reason for resource limits is preventing the noisy neighbor problem. Imagine a Java application with a memory leak that slowly consumes all available RAM on a host. Without a memory limit, the kernel's out-of-memory (OOM) killer will eventually step in and terminate processes—but it might kill your database container or your API gateway instead of the offending Java app. With properly configured limits, Docker terminates only the container that exceeds its memory allocation, leaving other services untouched.

Stability and Predictability

Resource limits make your infrastructure behavior predictable. When every container has a known maximum resource footprint, you can calculate exactly how many containers fit on a node. This is essential for capacity planning in orchestrated environments like Kubernetes, where the scheduler needs accurate resource availability data to place pods correctly.

Cost Control

In cloud environments, resource limits tie directly to billing. If you run containers on AWS ECS, Google Cloud Run, or Azure Container Instances, the platform charges you based on allocated resources. Setting limits ensures you don't accidentally provision more resources than intended, preventing surprise charges on your monthly bill.

Security Considerations

A container without resource limits can be exploited for denial-of-service attacks. An attacker who compromises one container can attempt to exhaust host resources, affecting all other applications. CPU and memory limits serve as a defense-in-depth measure that contains the blast radius of a compromised service.

How to Configure Docker Resource Limits

CPU Limits: Three Approaches

Docker offers multiple ways to restrict CPU usage, each suited to different scenarios.

1. CPU Shares (Relative Weighting)

The --cpu-shares flag assigns a relative weight to a container. The default value is 1024. A container with --cpu-shares=2048 gets roughly twice the CPU time as a container with 1024 shares when CPU contention occurs. This is a soft limit—if no other containers are competing for CPU, a container with low shares can still use 100% of the processor.

# Run a container with double the default CPU priority
docker run -d --cpu-shares=2048 --name high-priority-app nginx:latest

# Run a container with half the default CPU priority
docker run -d --cpu-shares=512 --name low-priority-app nginx:latest

2. CPU Quota (Hard Limit)

The --cpus flag (Docker 1.13+) sets an absolute hard limit on CPU usage. It accepts a decimal value representing the number of CPU cores the container can use. Internally, this sets both --cpu-period and --cpu-quota. For example, --cpus=1.5 means the container can use the equivalent of 1.5 CPU cores—it will be throttled if it tries to exceed that.

# Limit container to exactly 2 CPU cores
docker run -d --cpus=2 --name cpu-limited-app my-app:latest

# Limit container to half a CPU core
docker run -d --cpus=0.5 --name tiny-app my-app:latest

# The older, manual way: 50% of one CPU (period=100000, quota=50000)
docker run -d --cpu-period=100000 --cpu-quota=50000 --name old-style-app my-app:latest

3. CPU Set (Core Pinning)

The --cpuset-cpus flag restricts a container to specific CPU cores. This is useful for latency-sensitive applications that benefit from cache locality and want to avoid context switches across cores. You can specify individual cores or ranges.

# Pin container to CPU cores 0 and 1
docker run -d --cpuset-cpus="0-1" --name pinned-app my-app:latest

# Pin container to CPU cores 0, 2, and 4
docker run -d --cpuset-cpus="0,2,4" --name multi-core-app my-app:latest

Memory Limits: Hard, Soft, and Swap

Memory limits are arguably the most critical resource constraint in production. Docker supports a hard limit, a soft limit, and swap space control.

Hard Memory Limit

The --memory (or -m) flag sets an absolute ceiling on physical RAM. When a container exceeds this limit, Docker kills it with an OOM event. The limit accepts bytes, kilobytes (k), megabytes (m), or gigabytes (g).

# Limit container to 512 MB of RAM
docker run -d --memory=512m --name mem-limited-app my-app:latest

# Limit container to 1 GB of RAM
docker run -d --memory=1g --name larger-app my-app:latest

# Limit container to 256 MB with explicit byte notation
docker run -d --memory=268435456 --name precise-app my-app:latest

Soft Memory Limit

The --memory-reservation flag sets a soft limit that triggers memory reclamation (the container tries to free memory) but does not immediately kill the container. The soft limit must be lower than the hard limit. This is useful for services that can gracefully reduce their memory footprint, like caches with eviction policies.

# Soft limit at 256 MB, hard limit at 512 MB
docker run -d --memory=512m --memory-reservation=256m --name elastic-cache redis:latest

Swap Space Control

By default, Docker allows a container to use swap space equal to double its memory limit. You can customize this with --memory-swap. Setting --memory-swap to the same value as --memory disables swap entirely. Setting it to -1 allows unlimited swap (not recommended).

# Disable swap completely (container uses only physical RAM)
docker run -d --memory=512m --memory-swap=512m --name no-swap-app my-app:latest

# Allow 256 MB of swap in addition to 512 MB RAM (total: 768 MB)
docker run -d --memory=512m --memory-swap=768m --name with-swap-app my-app:latest

Monitoring OOM Events

When a container is killed due to memory exhaustion, Docker records the event. You can check for OOM kills using:

# Check if any container has been OOM killed
docker inspect mem-limited-app | grep -i oom

# The output will show "OOMKilled": true if it was killed

# View historical OOM events in logs
docker events --filter event=oom

Disk I/O Limits

Disk I/O limits prevent a single container from saturating storage bandwidth. These require the blkio cgroup controller and work on block devices. You can limit both the total bytes per second and the IOPS (input/output operations per second).

# Limit write speed to 10 MB per second on the primary block device
docker run -d --device-write-bps=/dev/sda:10mb --name io-limited-app my-app:latest

# Limit read speed to 50 MB per second
docker run -d --device-read-bps=/dev/sda:50mb --name read-limited-app my-app:latest

# Limit write IOPS to 100 operations per second
docker run -d --device-write-iops=/dev/sda:100 --name iops-limited-app my-app:latest

# Combine bandwidth and IOPS limits
docker run -d \
  --device-read-bps=/dev/sda:20mb \
  --device-write-bps=/dev/sda:10mb \
  --device-read-iops=/dev/sda:200 \
  --device-write-iops=/dev/sda:100 \
  --name fully-io-limited-app my-app:latest

Note that /dev/sda is a typical device name on Linux hosts. You can find the actual device with lsblk or df -h on the host. In containerized environments, you might target the device backing your volumes.

PID Limits (Process Count)

A fork bomb or a buggy application that spawns endless processes can exhaust the host's PID namespace. The --pids-limit flag caps the number of processes a container can create.

# Limit container to 100 processes
docker run -d --pids-limit=100 --name process-limited-app my-app:latest

# Use a high limit for services that legitimately need many processes
docker run -d --pids-limit=2000 --name high-process-app my-app:latest

# Default is unlimited (0), which should always be overridden in production
docker run -d --pids-limit=0 --name dangerous-app my-app:latest  # Avoid this!

When a container exceeds its PID limit, the kernel prevents it from forking new processes. The application receives an error (typically EAGAIN) on fork attempts, which well-written software can handle gracefully.

Using Docker Compose for Resource Limits

In production, you rarely start containers with raw docker run commands. Docker Compose provides a declarative way to configure resource limits that integrates with version control and CI/CD pipelines.

version: '3.8'
services:
  web:
    image: nginx:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
          pids: 50
        reservations:
          cpus: '0.25'
          memory: 128M
  
  api:
    image: my-api:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
        reservations:
          cpus: '1.0'
          memory: 512M
    
  database:
    image: postgres:15
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 4G
        reservations:
          cpus: '2.0'
          memory: 2G
    # Note: disk I/O limits are not supported in Compose v3
    # Use docker run directly or switch to a different orchestrator

Important caveat: the deploy section in Compose v3 was designed for Docker Swarm. If you're using docker-compose up (not Swarm mode), resource limits in the deploy section are ignored unless you use the --compatibility flag or switch to Compose v2 format with the mem_limit, cpu_shares fields directly on the service level.

# For non-Swarm docker-compose, use v2 syntax for guaranteed compatibility
version: '2.4'
services:
  web:
    image: nginx:latest
    mem_limit: 256m
    mem_reservation: 128m
    cpu_shares: 512
    cpu_quota: 50000
    pids_limit: 50
  
  api:
    image: my-api:latest
    mem_limit: 1g
    cpu_shares: 2048
    cpu_quota: 200000

Best Practices for Production Resource Limits

1. Always Set Memory Limits First

Memory is the most dangerous resource to leave unbounded. An unconstrained memory leak can bring down an entire host. Start by setting memory limits on every production container. If you're unsure of the right value, run the container in a staging environment under realistic load, monitor its memory usage with docker stats, and set the limit at roughly 125-150% of the observed peak. This gives headroom for spikes while still providing protection.

# Monitor container resource usage in real time
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" --no-stream

# For historical data, use a monitoring stack (Prometheus + cAdvisor)

2. Use Soft Limits with Hard Limits

For services with variable workloads, pair a soft limit (--memory-reservation) with a hard limit (--memory). The soft limit tells the kernel to reclaim memory when possible without killing the container, while the hard limit serves as a safety net. This pattern works especially well for caching services like Redis or Memcached.

# Redis with eviction policy to stay within limits gracefully
docker run -d \
  --memory=1g \
  --memory-reservation=512m \
  --name redis-cache \
  redis:latest \
  redis-server --maxmemory 900mb --maxmemory-policy allkeys-lru

3. Set CPU Limits Based on Application Profiles

Not all applications need CPU limits in the same way:

# Latency-sensitive API pinned to cores 2-5
docker run -d --cpuset-cpus="2-5" --memory=2g --name api-gateway api-gateway:latest

# Background worker with low CPU priority
docker run -d --cpu-shares=256 --memory=512m --name background-worker worker:latest

4. Never Set Limits Based on Guesswork

Resource limits should be data-driven. Run load tests against your container in a staging environment that mirrors production. Collect metrics on CPU, memory, and I/O usage under peak load. Use tools like Prometheus with cAdvisor, Datadog, or even simple docker stats output piped to a log file. Document the observed usage and set limits at 1.5x to 2x the peak for CPU and 1.25x to 1.5x for memory.

# Simple script to capture resource usage over time for analysis
for i in $(seq 1 300); do
  docker stats --no-stream --format "{{.Name}},{{.CPUPerc}},{{.MemUsage}}" my-container
  sleep 2
done > resource_profile.csv

5. Combine PID Limits with Memory Limits

A memory limit without a PID limit is incomplete. If a container hits its memory limit and the OOM killer triggers, the container dies—but what if it forks thousands of processes before hitting the memory ceiling? Those processes consume kernel resources beyond just RAM. Set --pids-limit to a reasonable value based on your application's legitimate process needs (typically 50-200 for most web services).

# Complete production-ready resource profile for a typical web service
docker run -d \
  --name production-web \
  --cpus=2.0 \
  --memory=1g \
  --memory-reservation=768m \
  --memory-swap=1g \
  --pids-limit=100 \
  --restart=unless-stopped \
  my-web-app:latest

6. Implement Health Checks Alongside Limits

Resource limits alone are reactive—they kill or throttle containers after thresholds are crossed. Combine them with Docker health checks to detect when a container is struggling before it reaches its limit. This gives orchestration systems time to react gracefully.

# Dockerfile health check definition
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

# Or in docker-compose
version: '2.4'
services:
  web:
    image: my-web-app:latest
    mem_limit: 1g
    cpu_shares: 2048
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

7. Use Docker Compose v2 for Non-Swarm Production

If you're not using Docker Swarm or Kubernetes, avoid Compose v3's deploy section for resource limits. It's silently ignored by docker-compose up. Instead, use Compose v2 format with explicit mem_limit, cpu_shares, and cpu_quota fields, which work consistently across all Docker Compose invocations.

# Compose v2.4 - resource limits work everywhere
version: '2.4'
services:
  app:
    image: my-app:latest
    mem_limit: 512m
    mem_reservation: 256m
    cpu_shares: 1024
    cpu_quota: 100000
    pids_limit: 75
    restart: unless-stopped

8. Monitor and Alert on Resource Utilization

Resource limits are not set-and-forget. Your application's resource usage will change over time as traffic patterns shift, code is updated, and data accumulates. Set up monitoring that tracks actual usage against configured limits. Alert when usage exceeds 80% of the limit so you can investigate and adjust before the limit triggers throttling or an OOM kill.

# Prometheus alert rule example for container memory
alert: ContainerMemoryUsageHigh
expr: |
  (container_memory_usage_bytes{namespace="production"} / 
   container_spec_memory_limit_bytes{namespace="production"}) > 0.8
for: 5m
labels:
  severity: warning
annotations:
  summary: "Container {{ $labels.container_name }} memory usage > 80% of limit"

9. Handle OOM Gracefully in Application Code

Even with perfect limits, OOM kills happen. Your application should be designed to recover cleanly. Ensure your process manager (supervisor, systemd, or Docker's restart policy) restarts the container automatically. Use --restart=unless-stopped or --restart=always. Write your application to flush buffers and close connections on shutdown signals, though note that OOM kills send SIGKILL, which cannot be caught.

# Always use a restart policy in production
docker run -d \
  --memory=512m \
  --restart=unless-stopped \
  --name resilient-app \
  my-app:latest

# For critical services, consider on-failure with a retry limit
docker run -d \
  --memory=1g \
  --restart=on-failure:5 \
  --name critical-app \
  my-app:latest

10. Test Limit Behavior Before Production

Before deploying to production, deliberately stress-test your containers against their configured limits. Use tools like stress, stress-ng, or custom load generators to push CPU and memory to the limit. Observe exactly what happens: Does the container throttle gracefully? Does it get killed? Do your health checks detect the degradation? Document the failure mode for each service.

# Test memory limit by running a stress container alongside your app
docker run --rm -d --memory=256m --name memory-test \
  progrium/stress:latest --vm 1 --vm-bytes 300M --timeout 30s

# Watch what happens when it exceeds the limit
docker events --filter container=memory-test

# Test CPU throttling
docker run --rm -d --cpus=0.5 --name cpu-test \
  progrium/stress:latest --cpu 2 --timeout 30s

# Observe throttled CPU in docker stats
docker stats cpu-test

Resource Limits in Kubernetes Context

If your production environment uses Kubernetes, the concepts translate directly to pod resource specifications. Kubernetes uses requests and limits in pod manifests, which serve the same purpose as Docker's soft and hard limits. The Kubernetes scheduler uses requests for placement decisions and enforces limits via cgroups, exactly as Docker does.

apiVersion: v1
kind: Pod
metadata:
  name: resource-limited-pod
spec:
  containers:
  - name: app
    image: my-app:latest
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "512Mi"
        cpu: "1000m"
        # Kubernetes doesn't natively support PID limits;
        # use PID namespace isolation or node-level configuration

Common Pitfalls and How to Avoid Them

Conclusion

Docker resource limits are a fundamental requirement for production container deployments. They protect your infrastructure from noisy neighbors, prevent cascading failures, enable accurate capacity planning, and serve as a security boundary. The core resources—CPU, memory, swap, disk I/O, and PIDs—each have dedicated configuration flags that map to Linux cgroup controllers. Memory limits should be your top priority, followed by CPU constraints appropriate to your workload type, and always capped with a PID limit. Pair soft and hard limits where possible, base your numbers on load-test data rather than guesswork, and verify that your orchestration layer actually enforces the limits you've declared. Combine resource limits with health checks, restart policies, and monitoring to build a resilient, predictable production container platform. The investment in configuring these limits pays for itself many times over by preventing the kind of production incidents that wake you up at 3 AM.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles