Introduction
In Part 2, we added network isolation to our container using namespaces.
But our container can still consume unlimited resources:
- All available CPU
- All available memory
- All available I/O bandwidth
A misbehaving container could crash the entire host.
In this part, we introduce Control Groups (cgroups) — the Linux resource management system that fixes this.
We will create a container that cannot use more than 50MB of RAM.
Step 1: Create a cgroup
First, let's create a directory inside the cgroup system directory — it will get populated automatically with the necessary files:
sudo mkdir /sys/fs/cgroup/my_container_limits
Verify the cgroup was created:
ls /sys/fs/cgroup/my_container_limits/
cgroup.controllers
...
cpu.max
cpu.pressure
cpu.stat
cpu.weight
cpu.weight.nice
io.max
...
memory.max
memory.min
memory.swap.max
...
pids.current
pids.events
pids.max
These files control and monitor resource usage.1 Since we care about memory, we'll focus on the files prefixed with memory. The one we want is memory.max — it sets the maximum amount of RAM a process (container) can consume.
Step 2: Set a Memory Limit
echo "50M" | sudo tee /sys/fs/cgroup/my_container_limits/memory.max
By default, even with memory.max set, the kernel can still use swap space to work around the limit. To enforce a truly hard cap, we disable swap as well:
echo "0" | sudo tee /sys/fs/cgroup/my_container_limits/memory.swap.max
Verify both limits were applied:
cat /sys/fs/cgroup/my_container_limits/memory.max
cat /sys/fs/cgroup/my_container_limits/memory.swap.max
52428800
0
The kernel stores its values in bytes — 50MB = 52,428,800 bytes.
Step 3: Start the Container
Assuming your container from Part 2 is already set up, just enter it:
sudo ip netns exec container_net chroot my_container /bin/bash
You are now inside the container.
Step 4: Get the Container's PID
Now — how do we tell the kernel which process this rule applies to? Simple: we write the process PID to the cgroup.procs file. Any PID written there becomes subject to the limits we set earlier.
From inside the container, get the shell's PID:
echo $$
12847
Step 5: Add the Process to the cgroup
From a second terminal on the host, assign the PID to the cgroup:
echo 12847 | sudo tee /sys/fs/cgroup/my_container_limits/cgroup.procs
12847
The container's bash process — and everything it spawns — is now subject to the 50MB limit.
Step 6: Monitor Memory Usage
To watch memory consumption live, you can use the following command:
watch -n 1 cat /sys/fs/cgroup/my_container_limits/memory.current
This prints current memory usage in bytes, refreshed every second.
Step 7: Test the Memory Limit
From inside the container, run something memory-intensive:
for i in {1..10000000}; do echo "$i"; done
1
2
3
...
...
Killed
Watch the number climb in your monitor terminal. The moment it crosses 50MB, the kernel's OOM (Out of Memory) killer 2 steps in and terminates the process.
There's a tool called stress that's purpose-built for this kind of testing — it simulates heavy load on your system by artificially consuming CPU, RAM, or disk I/O, so you can see your limits in action.
Alright, we successfully added resource accounting and enforcement to our container.
In this blog we only test it with RAM usage, but as you can see, our my_container_limits directory has all the files needed to control CPU, PID limits, and more.
This is exactly how Docker enforces --memory, --cpus, and --pids-limit under the hood. We just did it by hand.
With that, we've taken another step toward a more mature and complete container:
- Filesystem isolation — chroot
- Network isolation — network namespaces
- Resource limits — cgroups
Next, we cover OverlayFS — a cool technology that'll make our journey even more fun.
-
cgroup v2 uses a single unified tree to manage all controllers. Each file in that tree is either a setting you can tweak or a stat you can read. ↩︎
-
The OOM killer is a Linux kernel feature that forcefully kills processes when the system runs out of memory. Inside a cgroup, it only targets processes within that group. ↩︎