Docker & ContainersXXXIV Β· Capacity PlanningCapacity
Growth, headroom, and the quarterly capacity review
What you'll learn
- Project a growth trend into a date rather than a percentage
- Compute the N+1 headroom a fleet needs to survive losing a host
- Run a capacity review that produces a decision, not a dashboard
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
The four preceding lessons produced numbers for a single host at a single moment. This one turns those numbers into two things a manager can act on: a date by which the current capacity is exhausted, and a reserve that keeps the fleet up when a host dies.
βWe are at 60% and growingβ is not a capacity plan. βWe cross the 80% line on 14 March, and the procurement lead time is nine weeks, so we order in the second week of Januaryβ is.
From a slope to a date
The projection is one division:
days_to_threshold = (capacity x threshold - current) / daily_growth
Apply it to a disk carrying 57 GB on a 126 GB filesystem, growing at 420 MB a day, with the alert at 80%:
capacity x threshold = 126 x 0.80 = 100.8 GB
headroom remaining = 100.8 - 57 = 43.8 GB
daily growth = 0.42 GB/day
days_to_threshold = 43.8 / 0.42 = 104 days
104 days is mid-November. If the procurement or migration lead time is six weeks, the decision date is late September, and that date belongs in the calendar rather than in someoneβs memory.
STAMP=$(date +%F)
USED=$(df --output=used -B1 /var/lib/docker | tail -1)
IMG=$(docker system df --format '{{.Size}}' 2>/dev/null | head -1)
printf '%s\t%s\t%s\n' "$STAMP" "$USED" "$IMG" >> /var/log/docker-capacity.tsv
tail -3 /var/log/docker-capacity.tsv2026-08-09 60129542144 17.98GB
2026-08-10 60578234368 18.21GB
2026-08-11 61013622784 18.44GBIllustrative output
CAP=$(df --output=size -B1 /var/lib/docker | tail -1)
awk -v cap="$CAP" -F'\t' '
{ n++; x[n]=n; y[n]=$2; sx+=n; sy+=$2; sxy+=n*$2; sxx+=n*n }
END {
if (n < 7) { print "need at least 7 samples"; exit 1 }
slope = (n*sxy - sx*sy) / (n*sxx - sx*sx)
remain = cap*0.80 - y[n]
printf "growth: %.2f GB/day\n", slope/1073741824
printf "to 80%%: %.0f days\n", remain/slope
}' /var/log/docker-capacity.tsvgrowth: 0.42 GB/day
to 80%: 104 daysIllustrative output
Operational headroom: the arithmetic of N+1
Per-host utilisation targets from the CPU lesson assume the host stays up. Fleet capacity has to assume one does not.
If N hosts serve the load and you must survive F of them failing,
the survivors absorb everything:
utilisation_after_failure = U x N / (N - F)
Which inverts to the steady-state target you may actually run at:
U_steady <= U_max x (N - F) / N
| Fleet | Tolerating | U_max | Steady-state target |
|---|---|---|---|
| 2 hosts | 1 failure | 0.80 | 0.40 |
| 3 hosts | 1 failure | 0.80 | 0.53 |
| 5 hosts | 1 failure | 0.80 | 0.64 |
| 10 hosts | 1 failure | 0.80 | 0.72 |
| 5 hosts | 2 failures | 0.80 | 0.48 |
Read the first row again, because it is the one that surprises people: a two-host pair that must survive one failure can only be run at 40%. Half of every host is reserved for the other hostβs death. That is not waste; it is the price of the redundancy that was promised. The cheapest way to buy it back is more, smaller hosts β the ten-host fleet runs at 72% for the same guarantee.
FLEET=5
TOLERATE=1
SUM=$(docker ps -q | xargs -r docker inspect --format '{{.HostConfig.NanoCpus}}' | awk '{s+=$1} END {print s+0}')
CORES=$(nproc)
awk -v s="$SUM" -v c="$CORES" -v n="$FLEET" -v f="$TOLERATE" 'BEGIN {
u = (s/1e9)/c
printf "steady utilisation: %.2f\n", u
printf "after %d host loss: %.2f\n", f, u*n/(n-f)
printf "verdict: %s\n", (u*n/(n-f) > 0.80 ? "OVER BUDGET" : "within budget")
}'steady utilisation: 0.67
after 1 host loss: 0.84
verdict: OVER BUDGETIllustrative output
The largest-container constraint
The fleet-average calculation hides a second constraint. A container that needs 24 GB of memory can only be rescheduled onto a host with 24 GB free. Average headroom of 30% spread across five hosts as 6% each does not run that container anywhere.
largest_unit <= free capacity on at least one surviving host
Check it explicitly. It is the constraint that turns a routine host reboot into an incident, and no average will tell you about it.
The quarterly capacity review
The review is a meeting with a fixed input and a required output. Its purpose is a decision, not a status update.
- Inputs, gathered before the meeting. Peak (not mean) CPU, memory working set, disk used and inodes, conntrack peak, and the current growth slope for each β one table, one row per host or host class.
- Confirm the sizing hypotheses. For every container:
nr_throttled / nr_periods,memory.eventsoom_kill, andmemory.peakagainstmemory.max. Each is a pass or fail, not a discussion. - Reclaim first. Limits well above measured peaks are free capacity. Right-sizing usually recovers more than the next purchase would add, and it is available this week.
- Project each resource to its threshold date using the slope, and note which is nearest. That resource is the one the plan is about; the others are noise this quarter.
- Apply the N+1 constraint to the projected numbers, not the current ones. A fleet that is inside budget today and outside it at the projected date is already over budget.
- Check the largest-unit constraint against the projected free space per host.
- Produce a decision with a date. Order hardware, resize limits, shard the workload, or explicitly accept the risk with a review date. "Keep monitoring" is not one of the four.
Sanity check
Knowledge check Β· 4 questions
Q1. A three-host fleet must keep serving after one host fails, with a per-host ceiling of 80%. What is the maximum steady-state utilisation per host?
Q2. A filesystem is 126 GB, currently 57 GB used, growing 420 MB per day, with the alert threshold at 80%. When is the threshold crossed?
Q3. Which findings mean a fleet is over its capacity budget even though every host is currently below 80%? Select all that apply.
Q4. Fitting a straight line to a series that is actually growing exponentially produces a threshold date that is later than the real one.
Passing score: 75%. Answers are checked in this browser.