Docker Resource Limits and cgroups: Stop Your Containers From Eating the Host
Docker resource limits and cgroups control CPU, memory, and I/O.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Basic Docker usage (docker run, docker-compose)
- ✓Familiarity with Linux process management
- ✓Understanding of CPU and memory concepts
Use --memory, --cpus, --blkio-weight with docker run to limit resources. These map to cgroups v1 or v2. Always set memory limits to prevent OOM kills. For CPU, use --cpus for relative limits or --cpu-shares for proportional sharing.
Think of your server as a shared kitchen. Without limits, one cook (container) can hog all the stoves, fridges, and counter space, leaving others starving. cgroups are like assigning each cook a specific number of burners, a shelf in the fridge, and a time slot for the oven. No one can exceed their share, and the kitchen stays fair.
You've seen it happen: a memory leak in a container brings down the entire host. Or a CPU-bound process throttles your database container into submission. Docker resource limits exist to prevent exactly this chaos. But most developers slap on a --memory=512m and call it a day, not understanding that cgroups are a deep kernel feature with sharp edges. This article gives you the internals, the gotchas, and the production patterns that keep your containers from eating the host alive. By the end, you'll know exactly how to set limits, debug cgroup issues, and design resource isolation for real workloads.
What cgroups Actually Do Under the Hood
cgroups (control groups) are a Linux kernel feature that limits, accounts for, and isolates resource usage of process groups. Docker uses cgroups v1 (legacy) or v2 (newer) to enforce limits. Each container gets its own cgroup hierarchy under /sys/fs/cgroup/. When you set --memory=512m, Docker writes to memory.limit_in_bytes. The kernel enforces this at allocation time: if a process tries to allocate beyond the limit, it gets OOM-killed or throttled. CPU limits use Completely Fair Scheduler (CFS) quotas: cpu.cfs_period_us and cpu.cfs_quota_us. For example, --cpus=1.5 sets quota to 150000 in a 100000 period, meaning the container gets 1.5 cores worth of CPU time per period. Blkio limits throttle disk I/O using token bucket algorithms. Understanding these files lets you debug limits directly without Docker.
docker stats for quick live view, but for precise debugging, read cgroup files directly. They show raw kernel counters, not Docker's smoothed averages.Memory Limits: The Silent Killer
Memory limits are the most critical resource limit. Without them, a container can exhaust host memory, triggering the kernel OOM killer which may kill unrelated processes. Docker's --memory sets the hard limit. But there's a twist: --memory-swap defaults to twice the memory limit, meaning the container can use swap up to that total. This can mask memory leaks and cause performance degradation. Always set --memory-swap equal to --memory to disable swap. Also consider --memory-reservation — a soft limit that the kernel tries to enforce but can exceed under pressure. Use it for overcommit scenarios. Production gotcha: Java JVM doesn't respect cgroup memory limits by default in older versions. Use -XX:+UseContainerSupport and -XX:MaxRAMPercentage=75.0 to make it play nice.
under_oom 1 but no oom_kill, the container is in a reclaim loop — processes are stuck trying to free memory. This causes latency spikes. Increase memory or reduce workload.CPU Limits: Not All Cores Are Equal
CPU limits are trickier than they look. --cpus sets a hard cap: a container with --cpus=1.5 gets 1.5 CPU-seconds per second. But this is enforced via CFS quotas, which can cause throttling even when the host is idle. The kernel uses a 100ms period by default. If your container bursts CPU for 50ms then sleeps, it's fine. But if it uses 150ms continuously, it gets throttled for 50ms. This can hurt latency-sensitive apps. For proportional sharing (no hard cap), use --cpu-shares (default 1024). A container with shares=2048 gets twice as much CPU as one with 1024 when there's contention. But if the host is idle, it can use all cores. Use --cpus for predictable limits, --cpu-shares for fair sharing. Production gotcha: --cpus doesn't pin to specific cores. Use --cpuset-cpus for NUMA-aware pinning.
--cpus to a value higher than the number of physical cores. The container will be throttled constantly, and you'll see high nr_throttled in cpu.stat. Use --cpus <= host cores.Blkio Limits: Taming Disk I/O
Disk I/O limits are often overlooked until a batch job saturates the disk and your database latency spikes. Docker uses --blkio-weight for proportional I/O scheduling (similar to CPU shares). Range is 10-1000, default 500. A container with weight 1000 gets twice the I/O of one with 500 under contention. For hard limits, use --device-read-bps and --device-write-bps to cap throughput, or --device-read-iops and --device-write-iops for IOPS limits. These use the kernel's blkio cgroup controller. Note: blkio limits only apply to direct I/O, not buffered writes. Buffered writes are accounted to the page cache, which is shared. Production gotcha: On cgroups v1, blkio limits don't work with CFQ scheduler (deprecated). Use none scheduler or switch to v2.
--blkio-weight for most workloads. Reserve --device-*-bps for noisy neighbors that must be capped. Always test with oflag=direct to bypass page cache.cgroups v1 vs v2: What Changed and Why It Matters
cgroups v2 unified the hierarchy and fixed many v1 inconsistencies. Docker 20.10+ supports v2 on Linux 4.15+ with systemd. Key differences: v2 has a single hierarchy per controller, no more multiple mounts. Memory and CPU are in the same tree. The memory.limit_in_bytes becomes memory.max. CPU quota files are under cpu.max instead of cpu.cfs_quota_us. Blkio is replaced by io controller with io.max for limits. Docker abstracts most of this, but when debugging, you need to know which version your host uses. Check with stat -fc %T /sys/fs/cgroup/. If it says cgroup2fs, you're on v2. Production gotcha: Some tools like lxcfs or older monitoring agents may not work with v2. Also, swap accounting is disabled by default in v2; enable with swapaccount=1 kernel parameter.
--blkio-weight stops working. Use --device-write-bps instead, or switch to the io controller with docker run --io-max-read-bps (experimental).Resource Limits in Docker Compose and Kubernetes
Docker Compose uses the same syntax as docker run under deploy.resources. For example: ``yaml services: app: image: myapp deploy: resources: limits: cpus: '0.5' memory: 256M reservations: cpus: '0.25' memory: 128M ` In Kubernetes, resource limits are set in pod specs. But Kubernetes uses its own cgroup management (via kubelet). Docker's limits are ignored when running under Kubernetes. Instead, set resources.limits in the pod spec. Important: Kubernetes enforces limits via cgroups, but it also uses QoS classes (Guaranteed, Burstable, BestEffort) based on whether limits equal requests. For guaranteed QoS, set limits == requests`. Production gotcha: If you set CPU limits in Kubernetes, your container may be throttled even if the node has spare CPU. This is due to CFS quotas. Consider using CPU manager policies for latency-sensitive apps.
container_cpu_cfs_throttled_seconds_total in Prometheus.When Not to Use Resource Limits
Resource limits aren't always the answer. For single-container hosts or dedicated instances, limits add overhead and complexity. If your container is the only workload, skip limits and use host-level monitoring instead. Also, for batch jobs that need maximum throughput, limits can cause unnecessary throttling. Use --cpu-shares instead of --cpus to allow bursting. For memory, if your app has predictable usage, set a reservation but no hard limit — let the kernel OOM killer handle extreme cases. But this is risky. My rule: always set memory limits (hard or soft) for any container in a multi-tenant host. CPU limits are optional for low-priority workloads.
The 4GB Container That Kept Dying
--memory=4g but no --memory-swap limit. By default, swap is unlimited, so the container used 4GB RAM + 4GB swap. The kernel OOM killer killed the container when swap + RAM exceeded physical memory + swap space.--memory-swap=4g to match --memory, disabling swap. Or set --memory-swap=6g to allow some swap but cap total.- Always set
--memory-swapequal to--memoryunless you explicitly want swap. - Otherwise, containers can silently use swap and get OOM-killed unpredictably.
docker inspect for memory limit. 2. Check dmesg | grep -i oom for kernel messages. 3. Increase memory or set --memory-reservation. 4. If using Java, add -XX:+UseContainerSupport.cat /sys/fs/cgroup/cpu/cpu.stat inside container. 2. Increase --cpus or switch to --cpu-shares. 3. For Kubernetes, consider removing CPU limits.iotop. 2. Set --blkio-weight lower for batch containers. 3. Use --device-write-bps to cap throughput.docker inspect <container> --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}'dmesg | grep -i oom| File | Command / Code | Purpose |
|---|---|---|
| inspect_cgroups.sh | CONTAINER_ID=$(docker run -d --memory=512m alpine sleep 3600) | What cgroups Actually Do Under the Hood |
| memory_limit_example.sh | docker run -d --name memory-test \ | Memory Limits |
| cpu_limit_example.sh | docker run -d --name cpu-limited --cpus=1 alpine sh -c " | CPU Limits |
| blkio_limit_example.sh | docker run -d --name io-limited \ | Blkio Limits |
| check_cgroup_version.sh | stat -fc %T /sys/fs/cgroup/ | cgroups v1 vs v2 |
| docker-compose-resources.yml | version: '3.8' | Resource Limits in Docker Compose and Kubernetes |
Key takeaways
--memory-swap equal to --memory to disable swap and prevent OOM kills.--cpus cause throttling; use --cpu-shares for latency-sensitive apps.Interview Questions on This Topic
How does Docker enforce CPU limits using cgroups, and what happens when a container exceeds its quota?
cpu.cfs_quota_us to --cpus * 100000. When the container's CPU time exceeds the quota in a period, it's throttled until the next period. This can cause latency spikes. Use --cpu-shares for proportional sharing without hard caps.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Docker. Mark it forged?
3 min read · try the examples if you haven't