Shore Up
A workshop shelf crowded with dusty stacked crates, where a janitor removes only the ones marked with old dates and cobwebs, leaving the fresh crates in place.
Linux

Automate Container Image Cleanup on a Docker Host

Ketan Aagja9 min read
No ratings yet

Docker hosts fill up quietly. Every docker pull, every CI build, every image rebuild leaves layers behind, and /var/lib/docker grows until a deploy fails with "no space left on device" at the worst possible moment. This guide sets up a scheduled job that prunes unused images older than a threshold you choose, so the host reclaims space on its own without you babysitting it.

Before you run this

What it does: it runs docker image prune on a schedule to delete images that are not currently used by any container and that are older than a cutoff you set (a week, in the examples below). It reclaims disk space in /var/lib/docker.

This is destructive and effectively irreversible. Prune deletes image data. It does not touch running or stopped containers, and it will not remove an image that a container still references — but any image it does delete is gone from local disk. Recovery means re-pulling from a registry or rebuilding, which needs network access and the original tag or Dockerfile. If you keep locally-built images that are not pushed anywhere, understand that pruning them by mistake means rebuilding from source. Read the script, understand the filter, and run it once by hand on a non-production host or a test VM before you schedule it anywhere real.

Privileges: Docker commands need root, or a user in the docker group (which is equivalent to root — treat it that way). The scheduled job in this guide runs as root via systemd. Run the manual test with sudo.

A specific caution about -a: docker image prune -a removes all unused images, not just dangling (untagged) ones. That is the whole point here, but it means a tagged base image nothing currently runs will be removed. Combine it with the until time filter, as shown, so you only ever delete images past your cutoff — never something you pulled five minutes ago.

Assumptions: Ubuntu 22.04 LTS with Docker Engine (Community) installed from Docker's own apt repository, systemd as the init system, and a standalone host (not Kubernetes — kubelet has its own image garbage collection, don't run this alongside it). On RHEL/Alma the Docker commands are identical; only the earlier package-install steps differ, which are out of scope here.

Step 1: See what you'd reclaim, first

Before deleting anything, look at where the space is going:

# Summary of images, containers, volumes, build cache and reclaimable space
sudo docker system df

# Per-image detail, so you can see what's actually large and old
sudo docker images

docker system df tells you how much is "reclaimable" — that number is your realistic ceiling for image pruning. If it's small, you may not need this at all.

Step 2: Test the prune by hand

Run the exact command the scheduler will run, but do it manually first so there are no surprises. Start with a dry-ish approach: list what matches your cutoff before you delete.

# Show images created more than 168 hours (7 days) ago.
# This does NOT delete — it's a preview of candidates.
sudo docker images --filter "before=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%S)"

Note: Docker's image prune has no --dry-run flag. The docker images --filter before=... command above is the closest honest preview, and its matching is by image, not by the same "unused" logic prune uses — so treat it as a sanity check, not an exact list. There is no built-in way to have prune print-without-deleting; if you need a true dry run, verify the candidate set with docker images and your own judgement first.

Now the real command:

# Remove unused images older than 168h (7 days).
#   -a          : all unused images, not only dangling ones
#   -f          : don't prompt for confirmation (needed for a scheduled run)
#   --filter    : only images created before the cutoff
sudo docker image prune -a -f --filter "until=168h"

The until filter accepts a Go duration like 168h, or an absolute timestamp. Adjust 168h to your retention policy — 72h for three days, 720h for thirty. I deliberately do not use docker system prune --volumes here: that deletes volumes, and volumes hold data. Keep image cleanup and volume cleanup separate and deliberate.

Confirm it did something with sudo docker system df again and check the reclaimed space.

Step 3: Wrap it in a small script

Put the command in a script so the scheduler calls one thing and you can log it.

#!/usr/bin/env bash
# /usr/local/bin/docker-image-cleanup.sh
# Prune unused Docker images older than the retention window.

set -euo pipefail

# Retention window as a Go duration understood by Docker's `until` filter.
RETENTION="168h"   # 7 days — change to suit your host

echo "$(date --iso-8601=seconds) starting docker image prune (until=${RETENTION})"

# -a all unused images, -f no prompt, filtered to the retention window
docker image prune -a -f --filter "until=${RETENTION}"

echo "$(date --iso-8601=seconds) prune complete"
docker system df

Install it and make it executable:

sudo install -m 0755 docker-image-cleanup.sh /usr/local/bin/docker-image-cleanup.sh

Test the script directly before scheduling it:

sudo /usr/local/bin/docker-image-cleanup.sh

Step 4: Schedule it with a systemd timer

I use a systemd timer rather than cron here because you get logging in the journal for free and can inspect the last run easily. (Cron works equally well — a single line in /etc/cron.d/ calling the same script — but I'll show the systemd path.)

Create the service unit at /etc/systemd/system/docker-image-cleanup.service:

[Unit]
Description=Prune unused Docker images
# Only run once Docker is up
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/docker-image-cleanup.sh

Create the timer at /etc/systemd/system/docker-image-cleanup.timer:

[Unit]
Description=Run Docker image cleanup daily

[Timer]
# Every day at 03:30 local time
OnCalendar=*-*-* 03:30:00
# If the host was off at the scheduled time, run at next boot
Persistent=true

[Install]
WantedBy=timers.target

Enable and start the timer (the timer, not the service — the timer triggers the service):

sudo systemctl daemon-reload
sudo systemctl enable --now docker-image-cleanup.timer

Verify it's working

Confirm the timer is active and see when it will next fire:

# Should list docker-image-cleanup.timer with a NEXT run time
systemctl list-timers docker-image-cleanup.timer

Trigger the service once by hand to prove the whole chain works end to end, then read the log:

sudo systemctl start docker-image-cleanup.service

# The script's echo lines and the prune output land in the journal
journalctl -u docker-image-cleanup.service --no-pager -n 50

You should see the "starting" and "complete" lines and, from docker system df, a smaller reclaimable figure than before.

How to undo it

To stop the automation without deleting anything already reclaimed:

sudo systemctl disable --now docker-image-cleanup.timer

To remove it entirely:

sudo systemctl disable --now docker-image-cleanup.timer
sudo rm /etc/systemd/system/docker-image-cleanup.timer
sudo rm /etc/systemd/system/docker-image-cleanup.service
sudo rm /usr/local/bin/docker-image-cleanup.sh
sudo systemctl daemon-reload

Note again what you cannot undo: the images this job has already deleted. There is no local trash or restore for a pruned image — you re-pull or rebuild it. That is exactly why the until filter and the manual test in Step 2 matter. Set the retention window conservatively at first (say 720h), watch a few runs in the journal, and tighten it only once you're confident nothing you need is disappearing.

For the exact behaviour of the prune commands and their filters, see the official docker image prune and docker system df reference pages in the Docker documentation — they are the authority on any filter keyword you want to add beyond until.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.