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
- CPU: Control how much CPU time a container can consume (shares, quotas, or specific core assignments)
- Memory: Set a hard limit on RAM usage, plus a soft limit for more flexible memory management
- Swap: Control how much swap space the container can use once physical RAM is exhausted
- Disk I/O: Restrict read and write bandwidth on block devices
- PIDs: Limit the number of processes the container can fork
- Devices: Control access to hardware devices and their bandwidth
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:
- Batch processing / background jobs: Use
--cpu-sharesto give them lower priority so they don't interfere with user-facing services - Latency-sensitive APIs: Use
--cpuset-cpusto pin them to specific cores, reducing cache misses and context switches - Multi-threaded services: Use
--cpuswith a value matching the thread pool size to prevent over-provisioning - Go/C#/Node.js services: These typically benefit from
--cpus=2.0or more since garbage collection runs concurrently
# 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
- Setting only CPU limits: A container with CPU limits but no memory limits can still consume all RAM and trigger host-level OOM. Always set memory limits.
- Setting swap to -1: Unlimited swap allows a container to keep running while swapping heavily, degrading performance for the entire host. Always control swap explicitly.
- Ignoring PID limits: Fork bombs are real. A bug in application code or a malicious actor can exhaust the PID namespace even if memory is under limit. Set
--pids-limiton every container. - Using Compose v3 deploy limits without Swarm: These are silently ignored. Test your Compose file with
docker-compose upand verify limits appear indocker inspectoutput. - Setting limits too tight: Overly restrictive limits cause frequent throttling and OOM kills, leading to poor user experience. Profile your application under load first.
- Not accounting for shared libraries: Multiple containers sharing the same base image layers share memory for those libraries via the page cache. Your per-container limit doesn't account for this shared memory, which is a good thing—but understand it when calculating total host capacity.
- Forgetting about init processes: Containers with an init process (like
tiniordocker-init) consume a small amount of additional memory and one extra PID. Include this in your calculations.
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.