Configure BuildKit garbage collection and systemd timers to auto‑clean Docker images and volumes: a practical tutorial

Docker & Containers · By · Published on · 8 min read

HTML/CSS code snippet in editor on a monitor, colorful syntax and selective focus
Editor showing HTML/CSS code, conveys a technical and professional theme Photo: Bibek ghosh / Pexels

This tutorial shows how to set up BuildKit garbage collection and systemd timers to automatically clean Docker images, build cache and volumes on a VPS. You’ll apply safe policies, use labels for exclusions, and create systemd services/timers to control build cache, stale images and temporary volumes—avoiding surprises in production.

Prerequisites and what will be automated

The routine combines BuildKit GC (build cache) with controlled removal of Docker artefacts (stopped containers, old images and labeled volumes). The goal is to reduce disk usage without affecting running services.

Before proceeding, confirm you have sudo access, Docker and systemd active. Take a snapshot/backup if there is critical data. Some steps remove old or temporary data.

For safety, in-use images are not removed, and volumes are deleted only when labeled autoclean and not in use. If you still need to prepare the host, see how to install Docker and Compose on Linux in this guide.

  • Docker Engine and buildx installed
  • sudo access to create services/timers
  • Temporary free space to run cleanup
  • Backups or a host snapshot
Target Main command Criteria Notes
Build cache (BuildKit) docker buildx prune -af --keep-storage 20GB Keep N GB of cache Does not touch images/volumes; only build cache
Images docker image prune -a -f --filter 'until=240h' Age > 10 days and not in use May remove old unreferenced versions
Stopped containers docker container prune -f --filter 'until=240h' Age > 10 days and stopped Frees layers and old logs
Volumes docker volume rm $(docker volume ls -q --filter 'label=autoclean=true' --filter 'dangling=true') Only labeled autoclean and not in use Avoids accidentally deleting important data

What BuildKit actually cleans with GC?

BuildKit GC removes only build cache (layers and temporary artefacts produced during builds). It does not remove published images, containers, volumes or networks. We’ll use docker buildx prune with a storage limit to keep a useful, stable cache.

For images and volumes we’ll create separate routines with age filters and labels. This way each target has clear, independent rules.

How to check cache, images and volumes usage now

Before automating, measure current usage. buildx du shows build cache usage by repository/stage. docker system df details images, containers and volumes. List dangling volumes to identify low‑risk cleanup candidates.

# Uso do cache de build (BuildKit)
docker buildx du --verbose

# Uso geral de disco (imagens, containers, volumes)
docker system df -v

# Volumes sem uso (dangling)
docker volume ls --filter dangling=true

Configure BuildKit GC with systemd timers (storage-based policy)

We’ll schedule docker buildx prune to keep the build cache under a ceiling (keep-storage). Running via systemd ensures logs and that tasks are resumed if the host was off at the scheduled time.

If you use a dedicated builder (docker-container driver), adjust the builder target with the flag appropriate to your environment. Best practices for timers are in Migrate cron to systemd timers. To speed up builds with remote cache, see the guide on BuildKit remote cache for CI.

# Service e timer para GC do BuildKit (cache de build)
# Crie os arquivos abaixo

sudo tee /etc/systemd/system/buildkit-prune.service >/dev/null <<'EOF'
[Unit]
Description=BuildKit cache prune (docker buildx)
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
# Ajuste o limite conforme seu disco (ex.: 10GB, 20GB, 50GB)
ExecStart=/usr/bin/docker buildx prune -af --keep-storage 20GB
Nice=10
IOSchedulingClass=idle
EOF

sudo tee /etc/systemd/system/buildkit-prune.timer >/dev/null <<'EOF'
[Unit]
Description=Run BuildKit cache prune weekly

[Timer]
OnCalendar=Sun *-*-* 03:15:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now buildkit-prune.timer

Safe script to clean images and volumes by age and labels

The script below removes old stopped containers, prunes unused images and deletes only unused volumes labeled autoclean=true. Review filters before enabling in production. To protect critical images, use a keep=true label and the prune filter will preserve them.

# Dica: rotule recursos para controle fino (exemplos):
# docker volume create --label autoclean=true tmp_cache
# docker build -t minhaimg:latest --label keep=true .

sudo tee /usr/local/sbin/docker-gc-safe.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
log() { echo "[$(date -Is)] $*"; }

if ! docker info >/dev/null 2>&1; then
  log "Docker não está acessível; saindo."
  exit 0
fi

# 1) Containers parados antigos (ex.: >10 dias)
log "Pruning containers parados (>10d)"
docker container prune -f --filter "until=240h" || true

# 2) Imagens não usadas antigas, preservando as rotuladas com keep
log "Pruning imagens antigas não usadas (>10d), exceto label keep"
docker image prune -a -f \
  --filter "until=240h" \
  --filter "label!=keep" || true

# 3) Volumes sem uso e rotulados para autoclean
log "Removendo volumes sem uso com label autoclean=true"
mapfile -t VOLS < <(docker volume ls -q \
  --filter "label=autoclean=true" \
  --filter "dangling=true") || true
if ((${#VOLS[@]} > 0)); then
  docker volume rm "${VOLS[@]}" || true
else
  log "Nenhum volume elegível encontrado"
fi

log "Limpeza concluída"
EOF

sudo chmod +x /usr/local/sbin/docker-gc-safe.sh

How to schedule the cleanup script with a systemd timer

Create a oneshot service and a daily timer with a randomized delay to avoid collisions with backup windows. Persistent=true runs the task late if the host was off.

If you use Docker rootless, create units in the user systemd (systemctl --user) and store the script in your $HOME/bin. To integrate with your protection strategy, consider scheduling snapshots and uploads to object storage using timers, as in the LVM+Restic to S3 guide step-by-step.

sudo tee /etc/systemd/system/docker-gc.service >/dev/null <<'EOF'
[Unit]
Description=Rotina de limpeza segura de Docker (containers, imagens e volumes com label)
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-gc-safe.sh
Nice=10
IOSchedulingClass=idle
EOF

sudo tee /etc/systemd/system/docker-gc.timer >/dev/null <<'EOF'
[Unit]
Description=Agendar limpeza segura de Docker diariamente

[Timer]
OnCalendar=daily
RandomizedDelaySec=45m
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now docker-gc.timer

How to confirm it ran and where to check logs

Confirm active timers, read logs and measure space reduction with docker system df and buildx du. If a run was missed, Persistent=true will re-run it after system boot. That helps keep BuildKit GC and systemd timers reliable on your VPS.

# Timers ativos
systemctl list-timers '*buildkit*' '*docker-gc*'

# Logs da última execução
journalctl -u buildkit-prune.service -u docker-gc.service --since "-2d" -e

# Medir economia de espaço
docker system df -v
docker buildx du --verbose

Common issues and fine tuning

Some scenarios require adjusting filters. Prioritize labels for exceptions and review age windows in low‑deployment environments. With large private registries, run image pruning weekly and cache pruning daily.

  • Critical images disappearing: add label keep=true at build and preserve them with --filter label!=keep.
  • Insufficient build cache: increase --keep-storage (e.g., 30GB).
  • Volumes removed too early: label autoclean=true only on temporary volumes; never on database volumes.
  • Docker rootless: create timers with systemctl --user and ensure the user service is active.
  • Backup conflicts: schedule OnCalendar outside snapshot/backup windows.
  • No space to run prune: first clean only dangling resources (docker image prune -f) and, if needed, move large volumes to another disk following this migration guide step-by-step.

Frequently asked questions

Is it safe to run prune automatically in Docker?

Yes, when you set conservative filters and use labels for exceptions. Apply age filters (until=...) for containers and images and remove only unused volumes labeled autoclean=true. Have recent snapshots or backups before the first run.

What's the difference between docker system prune and buildx prune?

docker system prune removes Docker resources (stopped containers, unused images, networks and anonymous volumes). docker buildx prune removes only the BuildKit build cache. Combine both with distinct criteria and separate schedules.

How often should I run GC and timers?

Daily for build cache and stopped containers, and weekly for old images usually balances space and performance. Adjust age windows and keep-storage according to your deploy rate and disk size.

How do I prevent critical images from being removed?

Label them keep=true during the build and add --filter label!=keep to docker image prune. Marked versions stay preserved even if old and not referenced by containers.

Can I use this with Docker rootless?

Yes. Create the service and timer in the user systemd (systemctl --user) and ensure the user session is enabled on login. The rest (buildx prune, image prune, volume rm) works the same.

Will the timer run if the VPS was off at the scheduled time?

Yes. With Persistent=true, systemd triggers the missed run when the system returns. Logs are available in journalctl for audit and troubleshooting.

Want to standardize safe cleanup?

Need help reviewing filters, labels and schedules for your environment? Describe your scenario and we’ll validate policies together, with dry‑run and safe rollback.

Discuss GC policies

See also