Skip to main content
RunBook Academy

Docker & ContainersXXVIII ยท MaintenanceImage hygiene

Image hygiene on the host โ€” a retention policy you can defend

Intermediateโฑ ~22 mindocker

What you'll learn

  • Write down a host image retention policy in terms a prune command can execute
  • Identify the currently deployed image and the rollback image on a live host
  • Use build labels to make retention explicit rather than accidental
  • Recognise the failure modes of tag-based and age-based retention

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.

Ask an operator what images their production host should keep and you usually get a shrug and a prune command. That is not a policy โ€” it is a habit with a disk-usage trigger, and the reason so many rollbacks turn into re-pulls from a registry at the worst possible moment.

A defensible policy fits in three lines. This lesson is about writing those three lines and making a prune command obey them.

What a host actually needs to keep

For a host running services, the answer is short:

  1. Every image referenced by a container that exists โ€” running or stopped. Docker enforces this for you; a referenced image cannot be pruned.
  2. The previous release of each deployed application. This is the rollback, and Docker does not enforce it. Nothing references it the moment you replace the container.
  3. Base images your builds or deploys need offline. Only on hosts that build, and only if a registry outage must not stop a deploy.

Everything else is disposable. The interesting entry is number two, because it is the only one that requires a deliberate act.

Finding what is deployed and what is the rollback

Read-only / Safedeployed images
$ docker ps --format '{{.Names}}\t{{.Image}}' 
web       registry.example.com/myapp:release-2026-08
api       registry.example.com/myapi:release-2026-08
postgres  postgres:16.14-bookworm

Illustrative output

The .Image field here is the tag as written at create time. The container is bound to a digest, which is the value you need if you want to be certain:

Read-only / Saferesolve to digests
docker ps -q | xargs -r docker inspect     --format '{{.Name}} {{.Config.Image}} {{.Image}}'

The third field is the image ID the container is running. If a tag has moved since the container was created, .Config.Image and .Image disagree, and .Image is the truth.

Now the rollback candidate โ€” the previous release of the same repository:

Read-only / Saferelease history
$ docker image ls registry.example.com/myapp --format 'table {{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}'
TAG                ID             CREATED        SIZE
release-2026-08    0ef9d88b4b73   2 days ago     1.05GB
release-2026-07    bea532850da8   34 days ago    964MB
release-2026-06    69d2db152c39   66 days ago    964MB

Illustrative output

release-2026-07 is the rollback. No container references it. Every prune command with -a in it will delete it, and every age filter you are likely to pick will match it.

Expressing the policy with labels

Age and tag patterns both fail here. Labels do not.

Configuration changelabel at build
docker build --label 'org.example.retain=true' --label 'org.example.release=2026-08' -t registry.example.com/myapp:release-2026-08 .

Labels are baked into the image and travel with it through push and pull, so a host that pulls this image inherits the marking without any local state.

Destructivepolicy-driven prune
docker image prune -a -f --filter 'label!=org.example.retain=true'

That single command is the policy. It removes anything unreferenced and unlabelled, and it cannot touch a labelled release no matter how old.

Why the obvious alternatives fail

The check that makes it safe

Whatever the policy, one assertion belongs in front of every image prune on a host running services: the image the running container uses, and the image you would roll back to, both still exist afterwards.

Read-only / Safepost-prune assertion
set -euo pipefail

# Every image ID referenced by a container that exists
docker ps -aq | xargs -r docker inspect --format '{{.Image}}' | sort -u > /tmp/refs.before

docker image prune -a -f --filter 'label!=org.example.retain=true'

docker ps -aq | xargs -r docker inspect --format '{{.Image}}' | sort -u > /tmp/refs.after

if ! diff -q /tmp/refs.before /tmp/refs.after; then
echo 'FAIL: a referenced image disappeared' >&2
exit 1
fi
echo 'OK: all referenced images intact'

Docker will not let a prune remove a referenced image, so this check should never fire โ€” which is the point. It fires when something else changed under you: a container was removed by another process between the two snapshots, or a deploy ran concurrently. Both are worth knowing about before you walk away from the host.

A worked policy

For a host running two applications from a private registry:

RETAIN:
  - any image referenced by a container that exists      (Docker enforces)
  - the current and previous release of myapp and myapi  (label org.example.retain=true)
  - postgres:16.14-bookworm, the pinned database image   (labelled at pull time via re-tag)

PRUNE:
  - everything else, unreferenced, weekly

COMMAND:
  docker image prune -a -f --filter 'label!=org.example.retain=true'

VERIFY:
  every image ID in `docker ps -aq | xargs docker inspect -f '{{.Image}}'`
  still resolves after the prune

Three lines of intent, one command, one assertion. That is the whole policy, and it is enough to hand to whoever is on call.

Knowledge check

Knowledge check ยท 4 questions

  1. Q1. You replace the container for myapp with a new release. What is the status of the previous release image on the host?

  2. Q2. Why do image labels work better than age filters for expressing retention?

  3. Q3. Which references does Docker NOT protect from an image prune? Select all that apply.

  4. Q4. You can add a retention label to an existing local image with `docker image label`.

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