Docker containers are surprisingly inefficient with system resources by default, often consuming as much CPU and RAM as they can grab, which can starve other processes or even the host system itself.
Let’s see this in action. Imagine a simple Node.js app that just spins its wheels in a loop:
// app.js
let counter = 0;
setInterval(() => {
counter++;
// console.log(counter); // Uncomment to see it run, but it will flood logs
}, 1);
If we build this into a Docker image and run it without any limits, we can observe its resource usage:
# Build the image
docker build -t cpu-hogger .
# Run without limits and check resource usage
docker run -d --name hogger cpu-hogger
docker stats hogger
You’ll likely see the cpu-hogger container consistently using a significant percentage of your CPU, potentially 100% or more on a multi-core system, and its memory usage might climb steadily. This is because the container’s process thinks it has dedicated access to all the host’s resources.
To rein this in, Docker provides the --cpus and --memory flags during container creation. These flags allow you to specify hard limits on the CPU and memory a container can consume.
CPU Limits
The --cpus flag takes a floating-point number representing the number of CPU cores a container is allowed to use.
-
To limit a container to 1.5 CPU cores:
docker run -d --name limited-cpu --cpus 1.5 cpu-hoggerWhen you set
--cpus 1.5, you’re telling the Docker daemon to configure the container’s cgroup (control group) settings such that the process inside cannot utilize more than the equivalent of 1.5 CPU cores. This is achieved by adjusting thecpu.sharesandcpu.cfs_period_us/cpu.cfs_quota_usparameters within the container’s cgroup. Essentially, it throttles the CPU time allocated to the container’s processes. -
To limit a container to 50% of one CPU core:
docker run -d --name limited-cpu-half --cpus 0.5 cpu-hoggerThis is equivalent to setting
--cpus 0.5.
Memory Limits
The --memory flag sets a hard limit on the RAM a container can allocate. You can specify this in bytes or use suffixes like k (kilobytes), m (megabytes), or g (gigabytes).
-
To limit a container to 256 megabytes of RAM:
docker run -d --name limited-mem --memory 256m cpu-hoggerThis instructs Docker to set the
memory.limit_in_bytescgroup parameter for the container to 256,000,000 bytes. If the container’s processes attempt to allocate more memory than this limit, the kernel will start terminating processes within the container (specifically, the ones consuming the most memory) to stay within the bounds. This is the container’s "out-of-memory" (OOM) killer. -
To limit a container to 1 gigabyte of RAM:
docker run -d --name limited-mem-1g --memory 1g cpu-hoggerThis is equivalent to
--memory 1073741824.
Combining Limits
You can, and often should, combine both CPU and memory limits:
docker run -d --name limited-both --cpus 2 --memory 512m cpu-hogger
This container will be restricted to a maximum of 2 CPU cores and 512 megabytes of RAM.
Memory Swappiness
While setting a hard --memory limit is crucial, understanding how the kernel manages memory within that limit is also important. Linux uses a concept called "swappiness" to determine how aggressively it moves inactive memory pages from RAM to swap space (a dedicated disk partition or file). A high swappiness means the kernel will swap more readily, potentially leading to performance degradation if frequently accessed data ends up in swap.
For containers, you can control this by setting the memory.swappiness cgroup parameter. This parameter takes a value between 0 and 100. A value of 0 tells the kernel to avoid swapping as much as possible, while 100 means it will swap aggressively.
-
To set memory swappiness to a low value (e.g., 10) for a container:
docker run -d --name low-swap --memory 512m --memory-swappiness 10 cpu-hoggerThis tells the kernel to be very reluctant to swap out memory for this container, preferring to keep it in RAM as much as possible, even if it means potentially pushing other less critical data out of RAM. This can be beneficial for performance-sensitive applications that benefit from keeping their working set in physical memory. The default value for swappiness on most Linux systems is 60.
The next step is to understand how these limits interact with Docker Compose and Kubernetes, where they are defined in configuration files rather than command-line flags.