Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Limit Docker Container Resources

By Kokil Thapa | Last reviewed: August 2026

Unchecked containers are the most common cause of unexpected server outages in shared hosting environments. When you limit Docker container resources correctly, you prevent a single runaway PHP-FPM process or Node.js worker from starving your database and crashing the entire host. This guide covers the exact CLI flags, Compose configurations, and Cgroup v2 mechanics required to enforce hard boundaries on production Linux servers running Ubuntu 22.04 or 24.04.

How do you limit Docker container resources using the CLI?

The most direct way to constrain a container is at runtime using docker run flags. These map directly to Linux Control Groups (cgroups). On modern systems running Cgroup v2 (standard on Ubuntu 22.04+), these limits are enforced strictly by the kernel. If you are managing infrastructure for DevOps automation, understanding these primitives is essential before abstracting them into orchestration tools.

Setting Hard Memory Limits

The --memory (or -m) flag sets a hard ceiling. If the container exceeds this, the kernel invokes the OOM killer. For a Laravel application running PHP-FPM, a typical starting point might be 512MB to 1GB depending on worker count.

<!-- Run a Laravel app with strict 1GB RAM limit and 1.5 CPU quota -->
docker run -d \
  --name laravel-app \
  --memory=1g \
  --memory-swap=1g \
  --cpus=1.5 \
  my-laravel-image:latest
  • --memory: The maximum amount of RAM the container can use.
  • --memory-swap: Total memory (RAM + swap). Set equal to --memory to disable swap entirely. Disabling swap is usually preferred for web apps to avoid silent performance degradation.
  • --oom-kill-disable: Prevents the kernel from killing the container when it hits the limit. Use with extreme caution; the process will simply hang instead of restarting.

Configuring CPU Quotas

CPU limiting works differently than memory. Instead of a hard cap, Docker uses a quota system based on the Completely Fair Scheduler (CFS). The --cpus flag is the user-friendly wrapper introduced in recent Docker versions. A value of 1.0 equals one full core's worth of execution time per scheduling period.

You can also pin containers to specific physical cores using --cpuset-cpus="0,2". This is valuable on multi-tenant servers where you want to isolate noisy workloads from latency-sensitive databases. For teams evaluating cloud hosting providers, note that some VPS instances have non-uniform NUMA architectures where core pinning significantly impacts throughput.

CPU Quota vs Unlimited ExecutionLimited (--cpus=0.5)Active (50%)ThrottledActive (50%)ThrottledUnlimited (Default)Consumes All Available CyclesStarves Other ContainersResult: Predictable Multi-Tenancy
Visualizing how CPU quotas enforce fair scheduling compared to unconstrained containers that monopolize host resources.

How do you configure resource limits in Docker Compose?

In production, you rarely run raw docker run commands. Docker Compose is the standard for defining reproducible stacks. However, the syntax changed significantly between Compose V2/V3 and the current specification. As of 2026, with Docker Engine 27.x and Compose V2 integrated, you should use the deploy key even in standalone mode.

services:
  app:
    image: my-laravel-app:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M
  redis:
    image: redis:7.4-alpine
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'

Limits vs Reservations

This distinction causes more confusion than any other Docker configuration. Understanding it prevents both waste and instability:

  • Limits: The absolute maximum. The kernel enforces this. Exceeding memory triggers OOM kill; exceeding CPU causes throttling.
  • Reservations: A guaranteed minimum. The scheduler ensures this capacity is available before allocating to other containers. It does NOT reserve exclusive access if the container isn't using it.

On a server with 8GB RAM hosting multiple services, set reservations to what the app needs during idle/low traffic, and limits to what it needs during peak bursts. This allows efficient bin-packing while preventing catastrophic contention. For complex applications like those built with Laravel frameworks, profile your memory usage under load before setting these values.

What is the difference between Cgroup v1 and v2 for Docker?

If you are maintaining older servers or migrating legacy infrastructure, you must understand which cgroup version your host uses. Cgroup v2 has been the default on Ubuntu since 21.10 and RHEL since 8. In 2026, virtually all supported LTS distributions use v2 exclusively.

FeatureCgroup v1Cgroup v2
HierarchyMultiple independent hierarchiesSingle unified hierarchy
Memory AccountingSeparate kmem/slab countersUnified memory.events
Pressure MonitoringNot available nativelyNative PSI (Pressure Stall Information)
Docker Flag CompatibilityAll legacy flags workAll standard flags work; some deprecated
IO Throttlingblkio controllerio.controller (unified)

The practical impact for developers is minimal unless you are writing custom monitoring tooling. Docker abstracts the differences. However, if you see warnings about "cgroup v1 deprecation" during builds or startup, plan an OS upgrade. Newer kernels optimize v2 scheduling paths, meaning identical limits often perform better on v2 hosts.

Cgroup Architecture EvolutionCgroup v1 (Legacy)CPU HierarchyMemory HierarchyBlock IO HierarchyComplex, fragmented accountingCgroup v2 (Current)Unified Hierarchycpu / memory / io / pidsPressure Stall Info (PSI)Unified Event NotificationSimpler, accurate, kernel-optimized
Cgroup v2 consolidates resource controllers into a single hierarchy, enabling unified monitoring and more efficient enforcement.

How do you monitor and verify Docker resource limits in production?

Setting limits is only half the job. You must verify they are active and observe behavior under load. A common mistake I've encountered on client projects is configuring limits in Compose but forgetting to redeploy, leaving containers running with old unrestricted settings.

Inspecting Active Limits

Use docker inspect to confirm the kernel-enforced values. Do not rely on the Compose file alone.

# Check memory limit in bytes
docker inspect --format='{{.HostConfig.Memory}}' laravel-app

# Check CPU quota and period
docker inspect --format='Quota: {{.HostConfig.CpuQuota}}, Period: {{.HostConfig.CpuPeriod}}' laravel-app

# Human-readable summary
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"

If Memory returns 0, no limit is set. If CpuQuota is 0, CPU is unconstrained. Always validate after deployment.

Understanding OOM Kill Events

When a container hits its memory limit, the kernel kills processes inside it. Docker reports this as exit code 137. Monitor this explicitly:

# Check if container was OOM killed
docker inspect --format='{{.State.OOMKilled}}' laravel-app

# View kernel OOM messages
sudo dmesg -T | grep -i "killed process" | tail -20

# Check systemd journal for cgroup events
journalctl -u docker.service | grep "memory: usage"

If you see frequent OOM kills, either increase the limit or fix the application leak. For PHP applications, check pm.max_children and memory_limit in php-fpm.conf — these must align with container limits. Setting PHP memory_limit higher than container memory guarantees eventual crashes.

How do you limit Docker disk I/O and network bandwidth?

CPU and memory get most attention, but I/O starvation causes just as many production incidents. A backup job or log rotation script can saturate disk bandwidth, making your web application unresponsive even if CPU and RAM are fine.

Disk I/O Throttling

Use --device-read-bps and --device-write-bps to cap throughput per device. Identify the device path first with lsblk.

# Limit write speed to 50MB/s on /dev/sda
docker run -d \
  --device-write-bps /dev/sda:50mb \
  --device-read-bps /dev/sda:100mb \
  my-backup-service:latest

In Compose, this maps to blkio_config under the service definition. Note that I/O limits apply per-device, not globally. On NVMe systems with multiple namespaces, identify the correct path carefully.

Network Bandwidth Constraints

Docker does not natively support network bandwidth limiting via CLI flags. This requires either:

  • tc (traffic control): Apply qdisc rules to the container's veth interface. Complex but native.
  • Wondershaper / trickle: Run inside the container to self-limit.
  • Kubernetes/Orchestrator: If using K8s, CNI plugins like Cilium support egress/ingress rate limiting natively.

For most standalone Docker deployments, network saturation is less common than disk I/O. Focus on disk limits first unless you're running bandwidth-intensive media processing or proxy services.

Resource Limit Decision FlowContainer Misbehaving?OOM Kills / Exit 137→ Set --memory limitHigh Load / Slow Response→ Set --cpus quotaDisk Wait / High iowait→ Set device-*-bpsVerify: docker stats + docker inspect + dmesgConfirm limits active before considering issue resolvedIterate: Profile → Adjust → Monitor
Decision flowchart for diagnosing container resource issues and selecting the correct limiting mechanism.

Practical Resource Allocation Strategy

Theory matters less than repeatable patterns. Here is a baseline allocation strategy I use for typical LAMP/LEMP stacks on a 4-core, 8GB VPS. Adjust proportionally for your hardware.

  1. Database (MySQL/PostgreSQL): Reserve 30-40% of total RAM. Set hard limit to 50%. Database performance degrades catastrophically when swapped. Never starve this tier.
  2. Application (PHP-FPM/Node): Limit to 25-30% of RAM. Scale horizontally (more containers) rather than vertically (bigger container) when possible.
  3. Cache (Redis/Memcached): Hard limit to 10-15%. Redis evicts keys when hitting maxmemory; container OOM kills are worse than graceful eviction.
  4. Background Workers (Queues/Cron): Strict CPU limits (0.5-1.0 core). These are batch workloads that should yield to interactive requests.
  5. Reverse Proxy (Nginx/Traefik): Minimal limits (256MB, 0.5 CPU). Proxies are lightweight but critical; don't over-constrain.

Always leave 10-15% of host resources unallocated. The kernel needs breathing room for filesystem caches, network buffers, and SSH sessions. Running at 100% utilization means zero margin for spikes.

Conclusion

To effectively limit Docker container resources, combine hard limits with sensible reservations, verify enforcement with docker inspect, and monitor OOM events proactively. The configuration is straightforward; the discipline of profiling, testing, and iterating is what separates stable production systems from fragile ones. Whether you're running legal-tech portals, eCommerce platforms, or SaaS APIs, predictable resource isolation is foundational infrastructure work.

If you need help auditing your container configurations or designing a resilient deployment architecture for your production environment, reach out through my contact page. I regularly help teams stabilize their Docker deployments and optimize resource allocation for real-world workloads.

Frequently Asked Questions

Use the --cpus flag in docker run or the deploy.resources.limits.cpus key in Compose. For example, --cpus=1.5 restricts the container to 1.5 cores across all available host CPUs, preventing single-process monopolization while allowing burst capacity up to that ceiling during high load periods.

Memory limit is a hard cap causing OOM kills when exceeded, while reservation is a soft guarantee ensuring minimum availability without enforcing a maximum. In production Laravel environments, I always set both: reservation ensures baseline PHP-FPM worker stability, while limits prevent runaway processes from crashing the entire host server.

Typically 256MB to 512MB per worker process plus overhead.

Linux OOM killer triggers when actual RSS exceeds cgroup limits, often due to unaccounted shared libraries or child processes spawned by PHP exec calls. Check dmesg for OOM messages and verify your application isn't spawning unbounded subprocesses like ffmpeg or wkhtmltopdf without their own resource constraints inherited from the parent cgroup hierarchy.

Yes, using --device-read-bps and --device-write-bps flags with specific device paths. This prevents backup jobs or log-heavy Laravel queue workers from saturating disk throughput during peak hours. Note this requires knowing exact device identifiers via lsblk and works only on block devices, not overlay filesystems where most container writes actually occur.

Run docker stats to see live CPU, memory, network, and block I/O metrics per container. For persistent monitoring in production, I integrate cAdvisor with Prometheus and Grafana, which provides historical trends essential for right-sizing limits after deployment rather than guessing based on development environment behavior that rarely matches real traffic patterns.

The kernel throttles the container's cgroup CPU quota, causing processes to slow down proportionally rather than being killed. This manifests as increased response latency in Laravel applications under load. Unlike memory limits which trigger immediate termination, CPU throttling degrades gracefully but can cause request timeouts if limits are set too aggressively for your workload profile.

Use cpusets when you need strict core isolation for predictable performance, such as dedicating cores 0-3 to database containers and 4-7 to application containers. Use cpu-shares for proportional scheduling when workloads vary throughout the day. On shared EC2 instances hosting multiple Nepal client sites, I prefer cpusets to prevent noisy neighbor problems during peak business hours.

Define them under deploy.resources in your service definition. Limits enforce hard caps while reservations guarantee minimums. Remember that deploy section only applies in swarm mode; for standalone compose deployments, use the older mem_limit and cpus keys at the service level instead, as v3 deploy resources are ignored outside swarm orchestration contexts.

Absolutely. Resource limits are a critical defense-in-depth layer against denial-of-service attacks and compromised containers attempting cryptomining or fork bombs. Without limits, a single breached WordPress plugin or vulnerable Laravel endpoint could consume all host resources. Combined with read-only root filesystems and non-root users, limits contain blast radius significantly during security incidents.

Common causes include swap being enabled on the host (allowing containers to exceed RAM limits via swap), incorrect unit suffixes in configuration, or systemd cgroup delegation issues on Ubuntu 22/24. Verify with cat /sys/fs/cgroup/memory/memory.limit_in_bytes inside the container and ensure swap is disabled or explicitly limited via --memory-swap flag matching your RAM allocation.

Profile memory usage during peak job processing with docker stats over several days, then add twenty percent headroom for garbage collection spikes. Queue workers handling PDF generation or image processing need separate higher-limit services than simple email notification workers. In my experience deploying legal-tech portals, document-processing queues consistently require two to three times the memory of transactional queues running on identical codebases.

Run stress tests with wrk or k6 against staging environments while monitoring via docker stats and cAdvisor. Capture p95 and p99 resource utilization during realistic load scenarios, not synthetic benchmarks. For eCommerce platforms like those I've built for Nepalese grocery delivery, I simulate Dashain sale traffic patterns specifically because resource profiles during festivals differ dramatically from normal weekday operations.

Minimally for CPU limits, but memory limits can increase cold start times if set near actual requirements since the kernel must allocate and zero pages upfront. For Laravel applications using opcache preloading, insufficient memory limits cause repeated cache rebuilds on every restart. Always benchmark cold starts after adjusting limits, especially for auto-scaling environments where new containers spin up frequently during traffic spikes.

Use docker update to modify limits on active containers without restart. This works for CPU and memory adjustments in production emergencies. However, changes don't persist across container recreation, so update your Compose file or orchestration config immediately after. For zero-downtime deployments using Deployer 7, I bake tested limits into the deployment configuration to ensure consistency across releases and rollbacks.

Share this article

Quick Contact Options
Choose how you want to connect me: