Safely clean up images, volumes, and networks
Without a systematic Docker cleanup strategy, CI runners and production servers fill up within weeks with unused images, orphaned volumes, and stale build caches. The right automation concept prevents disk exhaustion without accidentally deleting important data.
Table of Contents
- 1. The Docker disk problem: where does the storage demand come from?
- 2. docker system df: understanding current disk usage
- 3. Cleaning up images: safely removing dangling and stale tags
- 4. Cleaning up volumes: removing orphaned data without data loss
- 5. Cleaning up networks and build cache
- 6. docker system prune: using the big cleanup safely
- 7. Automating Docker cleanup on CI runners
- 8. Docker cleanup on production servers: what is safe?
- 9. Cleanup strategies compared
- 10. Summary
- 11. FAQ
1. The Docker disk problem: where does the storage demand come from?
Docker accumulates storage in four categories: images, containers, volumes, and the build cache. Images are the largest and fastest growing category on CI servers, where multiple builds create new image tags every day. Every docker build call that cannot hit a layer cache adds new layers. If the old image is no longer referenced but also not explicitly deleted, it becomes a dangling image, visible as an image without a tag in docker images. These phantom images can add up to several gigabytes on an active CI runner within just a few days.
Volumes are the most insidious cleanup problem. A Docker cleanup with docker system prune does not delete volumes by default, this is intentional, to prevent accidental data loss. But anonymous volumes created by containers that were deleted long ago stay behind and quietly fill up disk space. On production servers, orphaned database volumes, test volumes, and temporary volumes from stopped Compose projects can occupy significant storage over months.
2. docker system df: understanding current disk usage
docker system df is the first tool for a systematic Docker cleanup. It shows an overview of all resource categories with total size, active share, and reclaimable space, in other words, what can be freed up through cleanup. With docker system df -v (verbose), it lists every individual resource: each image with its size, each container with its disk footprint, each volume with its allocation. This output immediately shows where the biggest savings potential lies.
An important distinction that docker system df makes clear during Docker cleanup: image size and actual disk usage can diverge significantly, because Docker images share layers. If two images use the same base layer, it is stored only once. This means deleting a single large image might only free up a few hundred megabytes if most of its layers are shared with other images. docker system df correctly reflects this reality in the shared size field.
# Overview: total, active, and reclaimable disk usage per resource type
docker system df
# Verbose: list all individual resources with their sizes
docker system df -v
# Quick summary: how much can be reclaimed right now?
docker system df --format "table {{.Type}}\t{{.TotalCount}}\t{{.Size}}\t{{.Reclaimable}}"
# Find the largest images (sorted by size)
docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}\t{{.ID}}" \
| sort -rh | head -20
# Find dangling images (untagged, not referenced by any container)
docker images -f dangling=true
# Find images older than 7 days (good candidates for cleanup)
docker images --format "{{.CreatedSince}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}" \
| grep -E "week|month|year"
3. Cleaning up images: safely removing dangling and stale tags
Dangling images, that is, images without a tag and without a referencing container, are the safest category for Docker cleanup. With docker image prune, only these unused intermediate layers get deleted. No running container and no tagged image is ever touched. This is the most conservative and safest cleanup step and can easily be automated daily. On an active CI runner with multiple builds per day, dangling image cleanup can free up several gigabytes daily.
For a more aggressive Docker cleanup, docker image prune -a deletes all images that are not referenced by any running or stopped container. This also affects tagged images that are not actively running in a container. With the filter --filter until=72h, only images older than 72 hours are removed, currently used images are preserved. This combination is ideal for CI runners that always build the latest images and no longer need older versions.
4. Cleaning up volumes: removing orphaned data without data loss
Docker cleanup for volumes requires more care than for images, because volumes hold persistent data. docker volume prune only deletes anonymous volumes that are not referenced by any running or stopped container. Named volumes created in Compose projects or with an explicit name are not deleted. That is the important safety net: a database whose data lives in a named volume (for example mysql-data) is not at risk from docker volume prune.
Before any production Docker cleanup of volumes, it is worth running docker volume ls and taking a look at the volume names. Named volumes such as project_mysql-data, project_redis-data, or project_uploads are clearly identifiable and are never touched by docker volume prune. Anonymous volumes have long, random hashes as names. With docker volume ls -f dangling=true, only non-referenced volumes are shown, which is the safe preview before the actual cleanup.
#!/usr/bin/env bash
# docker-cleanup.sh: Safe, automated Docker cleanup script
set -euo pipefail
echo "=== Docker Disk Usage Before Cleanup ==="
docker system df
echo ""
echo "=== Step 1: Remove dangling images (safe, no data loss) ==="
docker image prune -f
echo ""
echo "=== Step 2: Remove stopped containers older than 24h ==="
docker container prune -f --filter until=24h
echo ""
echo "=== Step 3: Remove unused networks ==="
docker network prune -f
echo ""
echo "=== Step 4: Remove build cache older than 48h ==="
docker buildx prune -f --filter until=48h
echo ""
echo "=== Step 5: Remove anonymous volumes not referenced by any container ==="
# Preview first
DANGLING_VOLUMES=$(docker volume ls -qf dangling=true | wc -l)
echo "Found ${DANGLING_VOLUMES} dangling volumes to remove"
docker volume prune -f
echo ""
echo "=== Docker Disk Usage After Cleanup ==="
docker system df
5. Cleaning up networks and build cache
Unused Docker networks mainly appear when Compose projects are started and stopped again without explicitly removing the networks. docker compose down without the --volumes flag leaves the networks in place by default. With docker network prune, all networks that are not used by any running container get removed. This is safe, because Docker automatically creates a new network whenever a Compose project or container needs one. The limit of 31 user-defined bridge networks per host (a Linux networking constraint) can cause errors if Docker cleanup for networks is neglected.
After images, the build cache is often the second largest consumer of storage. With docker buildx prune, the BuildKit cache can be cleaned up in a targeted way. Without a filter, it deletes the entire cache, meaning the next build has to rebuild every layer from scratch. With --filter until=48h, only cache entries older than 48 hours are removed. On CI runners that rely on BuildKit cache for fast builds, this is the more sensible option for automated Docker cleanup, since the current cache for running or scheduled jobs stays intact.
6. docker system prune: using the big cleanup safely
docker system prune is the most powerful, but also the riskiest, Docker cleanup command. In a single step it deletes: stopped containers, dangling images, unused networks, and the build cache. With -a, all unused images too. With --volumes, even all unused volumes. The command asks for interactive confirmation, which can be skipped with -f (force), necessary for automation, but something to use with care.
The most important rule for Docker cleanup with system prune: never run --volumes automatically on a production server without first making sure every relevant volume is a named volume and is covered by a backup. Anonymous volumes on a production server can hold data that no active container references, but that is still important, for example volumes from stopped containers that were only paused for maintenance work. Check beforehand with docker ps -a whether stopped containers exist that still hold relevant data in volumes.
# Aggressive cleanup for CI runners: remove all unused images older than 3 days
# Safe: running containers and their images are never touched
docker image prune -a -f --filter until=72h
# Remove all images not used by any container (use with caution on production)
docker image prune -a -f
# system prune without volumes (recommended for automation)
docker system prune -f --filter until=24h
# Full system prune including volumes: ONLY on disposable CI runners
# NEVER run this unattended on production servers
docker system prune -af --volumes
# Clean up Compose project completely (containers, networks, volumes)
docker compose -f docker-compose.yml down --volumes --remove-orphans
# List named volumes before any cleanup: verify nothing important is anonymous
docker volume ls --format "table {{.Name}}\t{{.Driver}}\t{{.Mountpoint}}"
7. Automating Docker cleanup on CI runners
CI runners are the environments where Docker cleanup is most urgently needed and, at the same time, safest to automate. Every build can create new images, containers, and build caches. Without regular cleanup, runner disks fill up within one to two weeks. The ideal strategy: after every CI job, the job's containers and networks are cleaned up (via Compose down within the job itself), dangling images and older unused images are removed daily via cron, and a more aggressive cleanup with docker system prune -af --filter until=24h runs weekly.
For GitLab CI and GitHub Actions, there is also the option of anchoring Docker cleanup directly into the pipeline: a cleanup stage at the end of every pipeline, separate from the actual build jobs, that always runs (with when: always in GitLab or if: always() in GitHub Actions). This ensures that even failed jobs do not accumulate unbounded cleanup debt. The cleanup job itself must not cause failures, it can be safeguarded with || true or continue-on-error: true.
8. Docker cleanup on production servers: what is safe?
On production servers, Docker cleanup should be approached more conservatively. Running containers and their images must never be removed. Stopped containers on a production server can be services that were temporarily paused. Named volumes hold persistent data. The only safe automated Docker cleanup on production servers is removing dangling images (docker image prune -f) and unused networks (docker network prune -f). Neither operation touches running containers or persistent data.
For removing old images on production servers, a manual or semi-automatic strategy is recommended: after deploying a new image, check whether the old image is still referenced by a container. If not, it can be removed. A cron job with docker image prune -f (dangling only) is safe. A cron job with docker image prune -a -f (all unused) is riskier on production servers and should be limited to at least --filter until=168h (one week), to leave enough time for rollback scenarios.
9. Cleanup strategies compared
The various Docker cleanup commands and strategies have different levels of aggressiveness and safety profiles. Choosing the right strategy depends on the environment (CI runner versus production server) and your tolerance for rebuild time.
| Command | What gets deleted | Safe on production? | CI runner recommendation |
|---|---|---|---|
image prune |
Dangling images only | Yes | Daily via cron |
image prune -a |
All unused images | With until filter | Daily with until=72h |
volume prune |
Anonymous, unreferenced volumes | Only if clearly anonymous | Weekly after review |
system prune |
Containers, images, networks, cache | Yes, without --volumes | Weekly |
system prune --volumes |
Everything including volumes | No | Disposable runners only |
The golden middle ground for automated Docker cleanup: docker image prune -f daily for dangling images, docker system prune -f --filter until=24h weekly for containers, networks, and old cache. Volume cleanup only manually or after review. CI runners can be handled more aggressively, production servers should always be treated more conservatively.
Mironsoft
Docker infrastructure, CI/CD automation, and server operations
Docker servers without disk exhaustion?
We set up automated Docker cleanup strategies that keep CI runners and production servers permanently clear, without accidental data loss, with a clear separation by environment and safety level.
Cleanup audit
Analysis of current storage usage and identification of the biggest savings potential
Cron automation
Tailored cleanup scripts with logging and alerting for CI and production
Registry cleanup
Automatic deletion of stale image tags in private Docker registries
10. Summary
Systematic Docker cleanup starts with understanding which resource category consumes the most storage. docker system df -v delivers this overview in seconds. The safest cleanup category is dangling images, followed by unused networks and stopped containers. Volumes require the most care, because they hold persistent data. Named volumes are protected from docker volume prune, only anonymous volumes get removed.
For automation, the rule of thumb is: CI runners can be cleaned up more aggressively, because they are disposable and images can be pulled again. Production servers need more conservative strategies with until filters and a focus on dangling images and networks. A daily cron job with docker image prune -f prevents the buildup of dangling images on every Docker host. The combination of targeted Docker cleanup per category and regular checks with docker system df keeps Docker hosts permanently healthy.
Automating Docker cleanup: the essentials at a glance
Diagnose first
docker system df -v shows total usage and reclaimable space per category. Always run it before cleanup.
Dangling images daily
docker image prune -f is the safest cleanup step. Set it up daily via cron for CI.
Volumes with caution
docker volume prune deletes only anonymous volumes. Named volumes are protected. Check docker volume ls first.
system prune safely
system prune -f --filter until=24h for automation. Never run --volumes unattended on production servers.