ServerlessBase Blog
  • Understanding Docker Resource Limits (CPU, Memory)

    Learn how to set CPU and memory limits for Docker containers to prevent resource exhaustion and improve system stability.

    Understanding Docker Resource Limits (CPU, Memory)

    You've probably seen containers crash or your development machine slow to a crawl when running multiple Docker containers. The issue usually isn't a bug in your application—it's that containers are consuming more resources than they should. Docker resource limits give you control over how much CPU and memory each container can use, preventing one misbehaving container from taking down your entire system.

    Why Resource Limits Matter

    Containers are lightweight, but they're not magic. Each container runs in its own process namespace with its own set of resources. Without limits, a poorly written application can consume all available CPU cycles or memory, causing the entire host to become unresponsive. This is especially problematic in production environments where you're running dozens or hundreds of containers on a single machine.

    Resource limits also help with fair resource allocation. When multiple teams or projects share the same infrastructure, limits ensure that one team's runaway application doesn't starve others of necessary CPU and memory. This is where the concept of "cgroups" comes in—Linux kernel features that Docker uses to enforce resource constraints.

    CPU Limits: How They Work

    Docker's CPU limits are expressed as relative shares, not absolute percentages. When you set a CPU limit, you're defining how much of the host's CPU cycles a container can use relative to other containers.

    CPU Shares vs CPU Quotas

    There are two ways to set CPU limits in Docker:

    CPU Shares (default: 1024) define a relative weight. A container with 2048 shares gets twice as much CPU time as a container with 1024 shares, assuming both are CPU-bound. This is useful for prioritizing certain containers over others.

    CPU Quotas (in microseconds per scheduling period) define an absolute limit. For example, setting --cpus="1.5" means the container can use up to 1.5 CPU cores over a 1-second period. This is more precise and predictable.

    Practical CPU Limit Examples

    # Give container 1 CPU core (relative to other containers)
    docker run --cpus="1.0" myapp
     
    # Give container 50% of available CPU (relative shares)
    docker run --cpu-shares=512 myapp
     
    # Limit to 2 CPU cores with 10% CPU time in each period
    docker run --cpus="2.0" --cpu-period=100000 --cpu-quota=10000 myapp

    The --cpus flag is the most common and easiest to use. It automatically calculates the appropriate --cpu-period and --cpu-quota values for you.

    CPU Limit Behavior

    When a container hits its CPU limit, Docker throttles its CPU usage. The container still runs, but it doesn't get more CPU time than it's allocated. This is different from OOM (Out of Memory) kills—CPU limits don't kill the container, they just slow it down.

    Here's what happens when a container exceeds its CPU limit:

    1. Docker tracks the container's CPU usage over a scheduling period (default: 100,000 microseconds)
    2. If the container uses more than its quota, Docker throttles it for the remainder of the period
    3. The container resumes normal operation at the start of the next period

    This throttling is transparent to the application. It just runs slower, which is often better than crashing or causing system-wide instability.

    Memory Limits: Preventing OOM Kills

    Memory limits are more critical than CPU limits because they can directly kill containers. When a container exceeds its memory limit, Docker kills it with an OOM (Out of Memory) kill signal.

    Memory Limit Syntax

    Memory limits are specified in bytes, but Docker also accepts human-readable formats:

    # Limit container to 512MB of memory
    docker run --memory="512m" myapp
     
    # Limit to 1GB
    docker run --memory="1g" myapp
     
    # Limit to 2GB with 256MB swap limit
    docker run --memory="2g" --memory-swap="2.5g" myapp

    The --memory-swap flag is particularly important. By default, --memory-swap is set to -1, which means the container can use as much swap as the host allows. Setting --memory-swap to the same value as --memory disables swap for the container, which can prevent OOM kills from being mitigated by swap.

    Memory Reservation vs Limit

    Docker also supports memory reservations, which are soft limits:

    docker run --memory-reservation="512m" --memory="1g" myapp

    The container can use up to 1GB of memory, but Docker tries to keep it around 512MB. This is useful for containers that have predictable memory usage patterns—you can reserve a baseline amount while still allowing bursts up to the limit.

    Memory Limit Best Practices

    1. Set memory limits for all containers in production, even if they seem small. You never know when an application might have a memory leak.

    2. Use memory reservations for containers with predictable memory usage. This helps with resource allocation and can prevent unnecessary OOM kills.

    3. Monitor memory usage with tools like docker stats to understand your container's actual memory consumption.

    4. Consider swap limits carefully. Disabling swap can make OOM kills more likely, but it can also prevent containers from thrashing to disk.

    5. Test your limits in staging environments before deploying to production. What looks like a safe limit in development might be too tight in production.

    CPU and Memory Together: The Complete Picture

    The most effective way to manage container resources is to set both CPU and memory limits:

    docker run \
      --name myapp \
      --cpus="1.5" \
      --memory="1g" \
      --memory-reservation="512m" \
      myapp:latest

    This configuration gives the container up to 1.5 CPU cores and 1GB of memory, while Docker tries to keep it around 512MB of memory. If the container needs more CPU, it can use up to 1.5 cores. If it needs more memory, it can use up to 1GB, but it will be killed if it exceeds that limit.

    Resource Limits in docker-compose

    When using Docker Compose, you can set resource limits in the docker-compose.yml file:

    version: '3.8'
     
    services:
      web:
        image: nginx:latest
        deploy:
          resources:
            limits:
              cpus: '1.5'
              memory: 1G
            reservations:
              cpus: '0.5'
              memory: 512M

    The deploy section is only used for Swarm mode, but the same syntax works for standalone Docker Compose with the --compatibility flag.

    Resource Limits in Kubernetes

    If you're using Kubernetes, resource limits are set in container specifications:

    apiVersion: v1
    kind: Pod
    metadata:
      name: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        resources:
          limits:
            cpu: "1.5"
            memory: "1Gi"
          requests:
            cpu: "0.5"
            memory: "512Mi"

    Kubernetes uses requests for scheduling decisions and limits for enforcement. This is different from Docker's reservations and limits, but the concepts are similar.

    Monitoring and Troubleshooting Resource Limits

    Viewing Resource Usage

    Docker provides built-in monitoring with docker stats:

    docker stats

    This shows real-time CPU and memory usage for all running containers. You can filter by container name:

    docker stats myapp

    Understanding Throttling

    If you suspect a container is being throttled, you can check Docker's throttling statistics:

    docker inspect --format='{{.HostConfig.CpuPeriod}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpusetCpus}}' myapp

    For more detailed information, you can use the Docker daemon stats endpoint:

    curl http://localhost:9324/stats?containers=all

    Common Issues and Solutions

    Issue: Container is being killed despite having CPU limits

    This is usually a memory issue, not a CPU issue. Check the container's memory usage with docker stats and adjust the memory limit accordingly.

    Issue: Container is running slowly but not being throttled

    The container might be CPU-bound but not hitting its limit. Check if other containers are using more CPU than expected, or if the host system is overloaded.

    Issue: Resource limits are not being enforced

    This can happen if you're running Docker in a container (nested Docker). Nested Docker doesn't have access to the host's cgroups, so resource limits won't work as expected.

    Practical Example: Setting Up a Production-Ready Container

    Here's a complete example of setting up a production-ready container with appropriate resource limits:

    docker run \
      --name production-app \
      --restart unless-stopped \
      --cpus="2.0" \
      --memory="4g" \
      --memory-reservation="2g" \
      --memory-swap="4g" \
      --pids-limit=100 \
      --network production-network \
      --volume app-data:/app/data \
      --volume app-logs:/app/logs \
      -e APP_ENV=production \
      -e LOG_LEVEL=info \
      myapp:production

    This configuration:

    • Limits CPU to 2 cores
    • Limits memory to 4GB with a 2GB reservation
    • Disables swap to prevent OOM kills from being mitigated
    • Limits the number of processes to 100
    • Uses a production network
    • Mounts persistent volumes for data and logs
    • Sets environment variables for production

    Conclusion

    Resource limits are essential for running containers reliably in production. They prevent one misbehaving container from taking down your entire system and ensure fair resource allocation across all containers. By understanding the difference between CPU shares and quotas, memory limits and reservations, and how to monitor container resource usage, you can build more stable and predictable containerized applications.

    When setting resource limits, start conservative and monitor your container's actual usage. Adjust the limits based on real-world data rather than guessing. Remember that resource limits are a tool for stability and predictability, not a way to squeeze every last drop of performance out of your containers.

    Platforms like ServerlessBase make it easy to set and manage resource limits for your applications, databases, and servers, so you can focus on building great software without worrying about resource exhaustion.

    Leave comment