Configure BuildKit garbage collection and systemd timers to auto‑clean Docker images and volumes: a practical tutorial
Docker & Containers · By Kevin Marcondes · Published on · 8 min read
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 policiesSee also
- Tutorial: set up mTLS between Traefik and Docker Compose services using smallstep CA and automatic certificates — step by step
- Tutorial: integrate Sentry into your GitHub Actions deploy pipeline — automatic source map upload & issue creation (PHP & JavaScript)
- Configure retention and compression for Docker container logs with journald + logrotate + rsyslog on a low-disk VPS
- Tutorial: Configure GitHub Actions for integration tests with PostgreSQL in ephemeral containers and automatic migration rollback — a practical guide