Skip to main content
RunBook Academy

Docker & ContainersXXVIII Β· MaintenanceScheduling

A maintenance schedule you can actually leave running

Intermediate⏱ ~24 mindockersystemd

What you'll learn

  • Separate the maintenance actions that are safe to automate from those that are not
  • Write a maintenance script that reports before and after every action
  • Schedule it with a systemd timer rather than cron, and read its outcome
  • Set disk thresholds that alert before maintenance becomes an incident

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

Not yet marked complete on this device.

The previous five lessons produced a set of commands. This one turns them into something that runs every week without you, which is the only form of maintenance that actually happens.

The design constraint is the whole lesson: an unattended prune has no operator to read the confirmation prompt, so the scoping has to be right before the -f goes on.

What is safe to automate, and what is not

ActionAutomate?Why
docker container prune --filter until=...YesRecreatable from a spec; the filter protects recent debugging state
docker image prune (dangling only)YesDangling images are superseded build output
docker builder prune with a boundYesCache rebuilds itself; a bound keeps it useful
docker network pruneYesUser-defined networks are recreated by Compose on next up
docker image prune -a with a label filterConditionallyOnly where the label discipline is real and enforced at build
docker image prune -a without a filterNoDeletes the rollback image
docker volume pruneNoAnonymous volumes hold real data on more hosts than you think
docker volume prune -aNoDeletes named volumes
docker system prune in any formNoAggregates four decisions into one command with one prompt

The line is not β€œdestructive versus safe”. Every command above is destructive. The line is recreatable versus not: a pruned image is a re-pull, a pruned build cache is a slow build, a pruned volume is a restore from backup if you have one and a resignation letter if you do not.

The script

Report, act, report. The two reports are what make the journal entry useful six weeks later when someone asks where an image went.

Destructive/usr/local/sbin/docker-maintenance
#!/usr/bin/env bash
set -euo pipefail

RETAIN_LABEL="org.example.retain=true"
CONTAINER_AGE="24h"
CACHE_AGE="168h"
CACHE_FLOOR="10GB"

echo "=== docker maintenance starting"
echo "--- before"
df -h --output=source,size,used,avail,pcent /var/lib/docker | tail -1
docker system df

echo "--- stopped containers older than $CONTAINER_AGE"
docker container prune -f --filter "until=$CONTAINER_AGE"

echo "--- dangling images"
docker image prune -f

echo "--- unreferenced images without the retain label"
docker image prune -a -f --filter "label!=$RETAIN_LABEL"

echo "--- unused networks"
docker network prune -f --filter "until=$CONTAINER_AGE"

echo "--- build cache older than $CACHE_AGE, keeping $CACHE_FLOOR"
docker builder prune -f --filter "until=$CACHE_AGE" --reserved-space "$CACHE_FLOOR"

echo "--- orphaned volumes (REPORT ONLY, never deleted here)"
docker volume ls --filter dangling=true --format '{{.Name}}' | wc -l
docker system df --format '{{.Type}} {{.Reclaimable}}' | grep -i volume || true

echo "--- after"
df -h --output=source,size,used,avail,pcent /var/lib/docker | tail -1
docker system df

echo "=== docker maintenance complete"

Three things about this script are deliberate:

  1. docker image prune -a carries a label filter. Without the filter it is the command that removes your rollback. With it, the retention policy from the previous lesson is what decides, and that policy is written down.
  2. Volumes are counted, never removed. The count is the alert. Somebody reads it and runs the audit.
  3. df brackets the whole run. docker system df tells you what Docker thinks; df tells you what the filesystem thinks. When they disagree, the gap is logs or bind mounts, which is exactly the finding you want.

The unit and the timer

Use systemd rather than cron. The reasons are practical: output lands in the journal with the unit name attached, Persistent=true catches up a run missed while the host was down, and systemctl status answers β€œdid it run and did it work” in one command.

Configuration change/etc/systemd/system/docker-maintenance.service
[Unit]
Description=Docker housekeeping - prune recreatable objects
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-maintenance
Nice=10
IOSchedulingClass=idle
Configuration change/etc/systemd/system/docker-maintenance.timer
[Unit]
Description=Weekly Docker housekeeping

[Timer]
OnCalendar=Sun 03:30
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target
Configuration changeenable
sudo systemctl daemon-reload
sudo systemctl enable --now docker-maintenance.timer
systemctl list-timers docker-maintenance.timer

Nice=10 and IOSchedulingClass=idle matter more than they look. Pruning several gigabytes of layers is I/O-heavy, and an unniced prune on a busy host adds latency to every container on it. The maintenance job should always lose to production traffic.

Reading the outcome

Read-only / Safeverify
# Schedule and last run
systemctl list-timers docker-maintenance.timer

# Exit status of the most recent run
systemctl status docker-maintenance.service

# Full output of the last run
journalctl -u docker-maintenance.service -n 200 --no-pager

# Just the last four weeks of before/after lines
journalctl -u docker-maintenance.service --since '4 weeks ago' --no-pager | grep -E 'before|after|reclaimed'

The last command is the one worth building a habit around. A month of before/after pairs tells you the growth rate, which is the number that decides whether the schedule is weekly or daily and whether the disk is big enough.

Thresholds and alerting

Scheduled maintenance is not a substitute for an alert. The schedule handles the predictable growth; the alert catches the day something grows a hundred times faster than usual.

Read-only / Safethreshold check
#!/usr/bin/env bash
set -euo pipefail

WARN_PCT=75
CRIT_PCT=90

USED=$(df --output=pcent /var/lib/docker | tail -1 | tr -dc '0-9')

if [ "$USED" -ge "$CRIT_PCT" ]; then
echo "CRITICAL: /var/lib/docker at ${USED}%"
docker system df
exit 2
elif [ "$USED" -ge "$WARN_PCT" ]; then
echo "WARNING: /var/lib/docker at ${USED}%"
exit 1
fi

echo "OK: /var/lib/docker at ${USED}%"

Set the warning low enough that maintenance is a scheduled task rather than an incident. 75% on the partition holding /var/lib/docker gives you days of notice on most hosts; 90% gives you hours.

The monitoring part of this course covers where these checks belong and how to route them. What matters here is that the threshold is on the filesystem, not on docker system df β€” because the consumers Docker does not account for, container logs above all, are the ones most likely to fill a disk overnight.

Knowledge check

Knowledge check Β· 4 questions

  1. Q1. Why should volume pruning stay out of an automated maintenance job?

  2. Q2. What does `RandomizedDelaySec=1800` in a systemd timer accomplish on a fleet?

  3. Q3. Which of these belong in an unattended weekly maintenance job on a production host? Select all that apply.

  4. Q4. A disk threshold alert should be set on the filesystem holding /var/lib/docker rather than on the totals reported by `docker system df`.

Passing score: 75%. Answers are checked in this browser.