Checkpoint Lab — CPU, Memory, PIDs, Devices, ulimits, cgroups v2, Reservations, and Container Resource Governance
Profile a synthetic workload, apply bounded resource controls, capture an evidence packet, and justify a reproducible container resource envelope.
Learning objectives
- Profile a synthetic workload before choosing limits.
- Predict and verify CPU, memory, PID, and ulimit effects independently.
- Capture Docker, cgroup, stats, and exit evidence in a reproducible packet.
- Select a justified resource envelope and document uncertainty, host capacity, and platform limitations.
1. Scenario and deliverable
You own a small synthetic worker. Your task is to measure its baseline, predict the effect of CPU/memory/PID/ulimit controls, apply those controls one at a time, and produce an evidence packet that another operator can use to reproduce the decision. The final recommendation must include host/VM capacity assumptions and at least one limitation.
2. Preflight and evidence directory
Use a dedicated local directory for text evidence. Do not store
secrets. The lab uses alpine:3.22 plus
python:3.13-alpine only for the bounded memory probe.
mkdir -p dkr22-evidence
docker context show | tee dkr22-evidence/context.txt
docker version | tee dkr22-evidence/docker-version.txt
docker info | tee dkr22-evidence/docker-info.txt
docker compose version | tee dkr22-evidence/compose-version.txt 2>&1 || true
docker buildx version | tee dkr22-evidence/buildx-version.txt 2>&1 || true
docker info --format 'cgroup={{.CgroupVersion}} driver={{.CgroupDriver}} cpus={{.NCPU}} memory={{.MemTotal}}' | tee dkr22-evidence/capacity.txt
docker pull alpine:3.22
docker pull python:3.13-alpine
docker image inspect alpine:3.22 --format '{{json .RepoDigests}}' | tee dkr22-evidence/alpine-digest.txt
docker image inspect python:3.13-alpine --format '{{json .RepoDigests}}' | tee dkr22-evidence/python-digest.txt
3. Write predictions before execution
Record at least two predictions before running the controlled workloads. A good packet states the expected mechanism and the evidence that will confirm or falsify it.
| Prediction | Expected evidence |
|---|---|
| A 0.50 CPU ceiling will create a quota in cgroups v2. |
HostConfig plus cpu.max; stats
should show bounded use, though exact samples vary.
|
| 64 MiB memory with equal memory-swap will not permit extra swap. |
HostConfig.Memory/MemorySwap;
failure state and memory.events if the probe
exceeds the working set.
|
| A PID ceiling will reject process/thread creation before unbounded host exhaustion. |
PidsLimit, pids.max, bounded spawn
errors/logs.
|
| An explicit nofile rlimit will be visible to PID 1. |
HostConfig.Ulimits and
ulimit -n//proc/1/limits.
|
4. Baseline and CPU-controlled workload
First run a short unrestricted CPU worker, capture one stats sample, then run the same loop under a half-CPU ceiling. This is not a scientific benchmark; it proves the policy/evidence chain. For performance conclusions, collect repeated samples under representative contention.
docker rm -f dkr22-base dkr22-cpu 2>/dev/null || true
docker run -d --name dkr22-base --label academy=docker-ch22 alpine:3.22 sh -c 'while :; do :; done'
sleep 2
docker stats --no-stream dkr22-base | tee dkr22-evidence/stats-baseline.txt
docker rm -f dkr22-base
docker run -d --name dkr22-cpu --label academy=docker-ch22 --cpus 0.50 alpine:3.22 sh -c 'while :; do :; done'
sleep 2
docker inspect dkr22-cpu > dkr22-evidence/cpu-inspect.json
docker stats --no-stream dkr22-cpu | tee dkr22-evidence/stats-cpu.txt
docker exec dkr22-cpu sh -c 'if [ -f /sys/fs/cgroup/cpu.max ]; then echo cpu.max=$(cat /sys/fs/cgroup/cpu.max); cat /sys/fs/cgroup/cpu.stat; fi' | tee dkr22-evidence/cpu-cgroup.txt
docker rm -f dkr22-cpu
5. Memory-controlled workload and exit evidence
The allocation loop is intentionally small and bounded by a 64 MiB cgroup. Preserve the failed container until after inspection. If the process raises allocation failure rather than being OOM-killed, record that result instead of forcing a different outcome.
docker rm -f dkr22-mem 2>/dev/null || true
docker run --name dkr22-mem --label academy=docker-ch22 --memory 64m --memory-swap 64m python:3.13-alpine python -c 'blocks=[]
while True: blocks.append(bytearray(8*1024*1024))' >dkr22-evidence/memory-stdout.txt 2>dkr22-evidence/memory-stderr.txt || true
docker inspect dkr22-mem > dkr22-evidence/memory-inspect.json
docker inspect dkr22-mem \
--format 'exit={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} memory={{.HostConfig.Memory}} memorySwap={{.HostConfig.MemorySwap}}' | tee dkr22-evidence/memory-summary.txt
docker rm dkr22-mem
6. PID and nofile controls
Use a small fixed number of child-process attempts and a short lifetime. Then prove a separate nofile rlimit. These tests demonstrate two different resource mechanisms.
docker rm -f dkr22-pids 2>/dev/null || true
docker run --name dkr22-pids --label academy=docker-ch22 --pids-limit 12 alpine:3.22 sh -c '
i=1
while [ "$i" -le 18 ]; do sleep 4 & echo "attempt=$i rc=$?"; i=$((i+1)); done
wait
' >dkr22-evidence/pids-stdout.txt 2>dkr22-evidence/pids-stderr.txt || true
docker inspect dkr22-pids > dkr22-evidence/pids-inspect.json
docker rm dkr22-pids
docker run \
--rm \
--name dkr22-ulimit \
--label academy=docker-ch22 \
--ulimit nofile=128:128 alpine:3.22 sh -c 'ulimit -n; cat /proc/1/limits | grep -i "open files" || true' | tee dkr22-evidence/ulimit.txt
7. Capture cgroup-v2 evidence when available
Use one short-lived inspection container with the intended final envelope. Reading the mounted cgroup controller files from inside the container avoids guessing the host cgroup path and works across systemd/cgroupfs layouts when the files are visible.
docker rm -f dkr22-envelope 2>/dev/null || true
docker run -d \
--name dkr22-envelope \
--label academy=docker-ch22 \
--cpus 0.50 \
--memory 128m \
--memory-swap 128m \
--pids-limit 64 \
--ulimit nofile=256:256 alpine:3.22 sleep 60
docker inspect dkr22-envelope > dkr22-evidence/envelope-inspect.json
docker stats --no-stream dkr22-envelope > dkr22-evidence/envelope-stats.txt
docker exec dkr22-envelope sh -c '
echo "proc-cgroup:"; cat /proc/self/cgroup
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
for f in cpu.max cpu.weight memory.max memory.high memory.swap.max pids.max memory.events; do
printf "%s=" "$f"; cat "/sys/fs/cgroup/$f" 2>/dev/null || echo unavailable
done
else
echo "cgroups-v2-files=unavailable"
fi
' > dkr22-evidence/envelope-cgroup.txt
docker rm -f dkr22-envelope
8. Choose and justify a resource envelope
Your conclusion must connect policy to evidence. A defensible statement looks like: “For this synthetic worker on a daemon reporting N CPUs and M bytes of memory, use 0.5 CPU, 128 MiB RAM with no extra swap, 64 PIDs, and 256 open files. This is a lab envelope, not a production sizing result; representative load and concurrency testing are still required.”
Do not claim a CPU or memory reservation from a standalone quota unless the target platform actually provides reservation semantics. Do not infer production capacity from one developer-laptop sample.
9. Verification checklist
- Exact Docker context, Engine/CLI versions, cgroup version/driver, host-or-VM CPU count, and memory capacity are recorded.
- Image repository digests for every workload image are recorded.
- CPU policy is proved by both Docker configuration and cgroup evidence where available.
-
Memory failure preserves exit code,
OOMKilled, logs, and configured memory/swap values. - PID and ulimit tests are bounded and their configuration is preserved.
- No privileged container, Docker socket mount, daemon mutation, broad prune, host firewall change, or production endpoint is used.
- The final resource envelope includes assumptions, uncertainty, and rollback criteria.
10. Cleanup and evidence archive
Remove only exact lab containers. Keep the evidence directory for
review. If you archive it, inspect the files before sharing because
docker info can contain host/runtime metadata even
though this lab uses no secrets.
docker rm -f dkr22-base dkr22-cpu dkr22-mem dkr22-pids dkr22-ulimit dkr22-envelope dkr22-device 2>/dev/null || true
docker ps -a --filter label=academy=docker-ch22
tar -czf dkr22-evidence.tgz dkr22-evidence/
11. What this adds to a production Docker operating model
You can now treat CPU, memory, PIDs, rlimits, and devices as measurable operating contracts rather than arbitrary flags. Production readiness means the image identity, host/VM capacity, resource policy, runtime evidence, and failure response are independently verifiable. Chapter 23 adds the next layer: logs, logging drivers, rotation, events, stats, and observability contracts.
Knowledge check
Why does the checkpoint record host/VM capacity before choosing a container envelope?
Because per-container limits operate inside that finite capacity. Without the denominator, a limit cannot be interpreted or capacity-planned correctly.
The memory probe exits but OOMKilled=false. Is the
lab invalid?
No. Preserve the allocator/application error and configured ceiling. The goal is to understand the actual failure mechanism, not force an OOM-kill result.
Why record both Docker HostConfig and cgroup files?
HostConfig proves the daemon declaration; cgroup files provide kernel-side enforcement evidence. Agreement between them strengthens the diagnosis.
Can this synthetic envelope be copied directly to production?
No. It demonstrates the method. Production sizing requires representative workload, concurrency, host contention, SLOs, and recovery testing.
A production container needs a GPU. What principle from this chapter applies?
Use a supported, narrowly scoped device/runtime reservation with exact identity and capability evidence; do not use privileged mode as a general solution.
Official references and version notes
- Docker Docs — Resource constraints — CPU and memory hard/soft controls, swap semantics, and OOM guidance.
-
Docker Docs — Runtime metrics
—
docker stats, cgroup discovery, and cgroups v2 behavior. - Docker CLI — docker container run — CPU, memory, PID, ulimit, device, and cgroup options.
- Docker CLI — docker container stats — CPU, memory, PIDs, I/O, and one-shot JSON/template output.
- Compose Deploy Specification — resources — limits, reservations, PIDs, and device reservations.
- Docker Desktop — Resources — CPU/memory/swap capacity of the Docker Desktop Linux VM.
- Docker Engine 29 release notes — current Engine baseline and recent runtime/ulimit changes.
-
Linux kernel — Control Group v2
— controller semantics for
cpu.*,memory.*, andpids.*.
Docker
Engine 29.8.1 is the current Engine baseline used here. Earlier
Engine 29 packaging moved to containerd 2.x; one important
consequence documented in Engine 29 release notes is that the
default container nofile limit can follow systemd's
default instead of older very-high Docker defaults. Docker Desktop
runs Linux containers inside a VM, so host capacity, cgroup paths,
devices, and swap evidence belong to that VM boundary. Record
docker version, docker info, and the
actual cgroup mode on your host rather than assuming these values.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.