Skip to main content
RunBook Academy

Docker & ContainersIV Β· ImagesLifecycle

Image lifecycle β€” pull, tag, push, prune, retain

Intermediate⏱ ~28 mindocker

What you'll learn

  • Pull and push images, and explain what each transfers and what it does not
  • State precisely what `docker image prune -a` considers unused
  • Build a retention policy that keeps your rollback target
  • Explain how registry-side garbage collection differs from host-side pruning
  • Move images between hosts without a registry

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-12

Not yet marked complete on this device.

An image is built, tagged, pushed, pulled, run, and eventually deleted. Five of those six steps are routine. The sixth is where estates lose the ability to roll back, and it is almost always run by somebody trying to be helpful about disk space.

Build and tag

Configuration changebuild
SHA=$(git rev-parse --short HEAD)
REPO=registry.example.com/myorg/myapp

docker buildx build \
--tag "$REPO:$SHA" \
--tag "$REPO:1.0.0" \
--load \
.

docker tag adds a name to an image that already exists. It copies nothing and creates no layer, so many tags on one image cost the disk of one image.

Push

Service impact possiblepush
$ docker push registry.example.com/myorg/myapp:1.0.0
The push refers to repository [registry.example.com/myorg/myapp]
9c1b6dd6c1e6: Pushed
4a2f0c8b7e31: Layer already exists
b8d2e0f4a917: Layer already exists
1.0.0: digest: sha256:0c1a2f9b7d3c4e5a6f7089abcdef0123d5f28ef21aabd54d6a48d8b9d3b8e5b1 size: 1789

Illustrative output

Layer already exists is deduplication doing its job: the registry stores blobs by digest, so ten images sharing a base upload it once. The digest: line is the value to record β€” it is the index digest, and it is what a pin should reference.

Pushing an image that is entirely unchanged transfers a manifest and nothing else. A suspiciously fast push is normal, not a sign something went wrong.

Pull

Read-only / Safepull
$ docker pull registry.example.com/myorg/myapp:1.0.0
1.0.0: Pulling from myorg/myapp
Digest: sha256:0c1a2f9b7d3c4e5a6f7089abcdef0123d5f28ef21aabd54d6a48d8b9d3b8e5b1
Status: Image is up to date for registry.example.com/myorg/myapp:1.0.0

Illustrative output

Status: Image is up to date means the tag still resolves to the digest you already have. Status: Downloaded newer image means it did not β€” which, on a tag you believed was stable, is drift you have just discovered by accident. Capture that line in deploy logs; it is a free drift detector.

docker run pulls implicitly when the image is absent. The --pull flag takes always, missing (the default) or never:

Configuration changepull policy
# Fails immediately if the image is not already on the host.
docker run --pull=never --rm registry.example.com/myorg/myapp:1.0.0 --version

--pull=never is worth considering for production, not because pulling is bad but because it moves the registry dependency out of the moment of failure. With missing, a container that gets recreated during an incident quietly acquires a dependency on the registry being reachable, at exactly the point you least want one. With never, the image is either staged or the command fails at once and tells you so.

Prune β€” the step that loses your rollback

Destructiveprune
# Safe: dangling only.
docker image prune

# Aggressive: everything no container references, older than 30 days.
docker image prune -a --filter "until=720h"

# Protect a set with a label applied at build time.
docker image prune -a --filter "label!=retain=true"
Read-only / Safedry run
# Every image ID currently referenced by a container, running or not.
IN_USE=$(docker ps -aq | xargs -r docker inspect --format '{{.Image}}' | sort -u)

echo "Would be removed:"
docker image ls --format '{{.ID}} {{.Repository}}:{{.Tag}}' | while read -r id ref; do
  full=$(docker image inspect --format '{{.Id}}' "$id")
  echo "$IN_USE" | grep -q "$full" || echo "  $ref ($id)"
done

Run that, read it, and only then decide. On a production host the output should be short enough to read; if it is not, that is itself the finding.

A retention policy that keeps the rollback

  1. Label every image at build time with something you can filter on: --label retain=true for release builds, nothing for CI scratch builds.
  2. Prune dangling images aggressively and on a schedule. That is the bulk of the waste on a build host and it is safe.
  3. Prune -a only with a until= window long enough to cover your rollback horizon. If you might roll back a month, do not prune anything younger than a month.
  4. Before any planned prune, record the digests currently deployed: docker ps -q | xargs -r docker inspect --format "{{.Image}}". That list is your recovery plan.
  5. Keep the current and the previous release digest referenced somewhere the prune cannot reach β€” a tag in the registry, and ideally a docker save tarball on separate storage for the current release.
  6. Never run docker system prune -a from a scheduled job on a production host. The container-removal step makes its blast radius depend on what happened to be running when it fired.
  7. Monitor /var/lib/docker free space so pruning is a planned action and never an emergency one. Every bad prune in this lesson was run by somebody under disk pressure.

Registry-side retention is a different mechanism

Deleting a tag in a registry does not free space. The blobs remain until garbage collection runs, and for the reference distribution registry that is a manual command, not a scheduled policy:

Destructiveregistry GC
# Requires storage.delete.enabled in config.yml, and the registry
# should be stopped or in read-only mode while this runs.
registry garbage-collect --dry-run /etc/distribution/config.yml

# --delete-untagged also removes manifests that no tag points at.
registry garbage-collect --delete-untagged /etc/distribution/config.yml

Two consequences follow.

A digest pin can outlive its content. --delete-untagged removes exactly the manifests that a digest-only reference depends on. If you pin a digest, keep a tag on it too, or the registry’s own housekeeping will eventually break your deployment.

Disk on the registry does not shrink when you delete tags. Teams regularly delete hundreds of tags, see no change in storage, and conclude the deletion failed. It did not; GC has not run.

Hosted registries β€” Docker Hub, ECR, GCR, Harbor, Artifactory β€” each implement their own retention rules, with their own definitions of what is eligible. Read yours before you rely on a digest surviving. Do not assume the semantics of one transfer to another.

Moving images without a registry

Configuration changesave and load
IMAGE=registry.example.com/myorg/myapp:1.0.0

# On the connected host.
docker save "$IMAGE" | gzip > myapp-1.0.0.tar.gz
sha256sum myapp-1.0.0.tar.gz > myapp-1.0.0.tar.gz.sha256

# On the target host, after transferring both files.
sha256sum -c myapp-1.0.0.tar.gz.sha256
gunzip -c myapp-1.0.0.tar.gz | docker load

docker image inspect --format '{{.Id}}' "$IMAGE"

docker save preserves the tags and produces a self-contained archive. It is the cheapest insurance against the prune scenario above: a tarball on separate storage is a rollback target that no host-side command can remove.

docker export is a different thing and is regularly confused with it β€” it writes a container’s flattened filesystem with no layers, no history, and no image configuration, so an export followed by import loses the entrypoint, the environment, and the user. Use save/load for images and export/import only when you specifically want a flattened snapshot.

Verification that can fail

Read-only / Saferollback readiness
FAIL=0

for id in $(docker ps -q); do
  name=$(docker inspect --format '{{.Name}}' "$id")
  img=$(docker inspect --format '{{.Image}}' "$id")
  if ! docker image inspect "$img" >/dev/null 2>&1; then
      echo "FAIL $name runs an image that is no longer in the local store" >&2
      FAIL=1
  fi
done

if [ "$(docker image ls --filter 'label=retain=true' -q | wc -l)" -lt 2 ]; then
  echo "FAIL fewer than two retained releases on this host: no rollback target" >&2
  FAIL=1
fi

[ "$FAIL" -eq 0 ] && echo "rollback target present"
exit "$FAIL"

Knowledge check

Knowledge check Β· 6 questions

  1. Q1. What exactly does `docker image prune -a` remove?

  2. Q2. Why does `docker compose down` before a prune widen the blast radius compared to `docker compose stop`?

  3. Q3. Which of these does `docker system prune` remove by default, without any flags? Select all that apply.

  4. Q4. Deleting hundreds of tags from a distribution registry can free no disk space at all until garbage collection is run by hand.

  5. Q5. You need to move an image to an air-gapped host and keep it as a rollback artefact. Which command?

  6. Q6. A running container shows `<none>:<none>` for its image in `docker image ls`. What has happened, and what is the risk?

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