Every CI build adds an image, and registries never delete on their own. Distribution deletes in two steps: an API call removes a manifest by digest, and registry garbage-collect later removes the blobs that no manifest references (the registry:3 image enables deletion by default):
docker exec l3-registry du -sh /var/lib/registry
ACCEPT='Accept: application/vnd.oci.image.index.v1+json'
D=$(curl -sI -H "$ACCEPT" localhost:33500/v2/booknest/manifests/ghcr-latest \
| awk 'tolower($1)=="docker-content-digest:" {print $2}' | tr -d '\r')
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:33500/v2/booknest/manifests/$D
curl -s localhost:33500/v2/booknest/tags/list
docker exec l3-registry registry garbage-collect --delete-untagged \
/etc/distribution/config.yml 2>&1 | grep -c 'eligible for deletion'
docker exec l3-registry du -sh /var/lib/registry79.3M /var/lib/registry
202
{"name":"booknest","tags":[]}
26
78.4M /var/lib/registryThe delete was accepted (202) and the tag list emptied, yet 26 eligible links freed under 1 MB: the copy shares its base layers with booknest-api, and a blob goes only when nothing references it. Collect with the registry stopped or read-only, since a concurrent push can lose blobs. Hosted registries automate retention: actions/delete-package-versions (https://github.com/actions/delete-package-versions 445 ) for GitHub 29 , lifecycle rules by age, count and tag in ECR 24 , Artifact Registry and Harbor 109,941 .