Registry Garbage Collection: How Self-Hosted Docker Registries Actually Reclaim Space
AI generated
FROM
RUN
Docker · Registry · Operations
Registry Garbage Collection
Keeping self-hosted registry storage growth in check

docker system prune only cleans up locally; the growth of a self-hosted Docker registry with thousands of pushed images is completely unaffected by it. Registry-side garbage collection and thoughtful retention policies are the actual lever against runaway storage consumption.

17 min read Docker Registry Operations

1. Why docker system prune does not help here

A widespread misconception is that docker system prune or docker image prune also reduces the storage used on the registry server. In reality, these commands act exclusively on the local machine or runner where they are executed, cleaning up unused images, stopped containers, and orphaned layers there. The storage of a self-hosted registry, for example based on Docker Distribution, Harbor, or GitLab Container Registry, remains completely unaffected, because the registry is a standalone service with its own storage backend, usually an object store like S3 or a local filesystem.

In an active CI/CD pipeline with several builds a day, each tagged with unique Git SHA or date tags as described in the previous article, a registry without its own cleanup strategy keeps growing continuously, even though the vast majority of these images are never pulled again after a short time. Without a countermeasure, this leads over time to sharply rising storage costs, longer backup times, and in extreme cases to fully saturated storage volumes that can bring registry operations to a complete halt.

2. How a registry stores images internally

A Docker registry does not store images as monolithic files but as a set of content-addressed blobs corresponding to the individual layers and the manifest file, plus a mapping table that maps tags to manifest digests. Multiple tags, or even multiple different images, can share the same blob, for example a common base layer like php:8.4-fpm-alpine, so the registry efficiently occupies storage for identical content only once, regardless of how many tags ultimately point at it.

This exact structure is what makes deletion complicated: simply removing a tag usually only deletes the mapping in the manifest index, not the underlying blobs, because at the moment a tag is deleted the registry cannot readily know whether another tag or another image still references the same blob. This is exactly where actual garbage collection comes in, determining in a separate pass all blobs still referenced and physically removing from disk only the blobs that are genuinely orphaned.

3. Garbage collection in Docker Distribution in detail

The free reference implementation Docker Distribution, on which many self-hosted registries are built, comes with a built-in garbage collector invoked via the registry garbage-collect command inside the registry container. The process consists of two phases: first a mark pass runs, which starting from all still-existing tags recursively marks every referenced manifest and layer digest, followed by a sweep pass that physically deletes all unmarked blobs.

It is important that the registry be run in read-only mode during the garbage collection run, unless the newer version with --dry-run and delete-marker support is used, since otherwise images pushed concurrently could be incorrectly marked as orphaned and deleted. In production setups, the GC run is therefore usually executed during a maintenance window or with the newer delete-marker strategy, which allows safe deletion even while the registry keeps running.


# Garbage collection in Docker Distribution (classic workflow)
docker exec registry bin/registry garbage-collect \
  --dry-run /etc/docker/registry/config.yml

# After reviewing the dry run, actually delete
docker exec registry bin/registry garbage-collect \
  /etc/docker/registry/config.yml

4. GC performance and runtime on large registries

On a small internal registry with a few hundred images, a garbage collection run usually takes only seconds to a few minutes, but on large registries with tens of thousands of repositories and millions of blobs, the mark pass, which must resolve and recursively walk every manifest individually, can take several hours. If GC runs in read-only mode during that time, it effectively means a multi-hour outage of all push operations, which is hardly acceptable for active CI/CD landscapes.

As a countermeasure, larger deployments usually rely on the delete-marker strategy, which initially only records deletions as a marker and runs the actual physical sweep in a separate, clearly scheduled maintenance window, as well as on an object storage backend such as S3 instead of a local filesystem, since object storage APIs support parallel, batched access to millions of blobs far more efficiently than a single local filesystem with a correspondingly high number of inodes.

5. Setting up retention policies for old tags

Garbage collection alone only physically deletes tags that were already removed; it does not decide which tags should be removed in the first place. That job belongs to a retention policy, defining rules such as: keep the last ten tags per repository, delete tags older than ninety days except the last five SemVer releases, or keep all tags starting with a protected prefix like release-. Without such a policy, the number of tags keeps growing indefinitely, even if the underlying blobs are partially shared through deduplication.

In registries such as Harbor or GitLab Container Registry, retention policies can be defined declaratively via the web UI or a YAML configuration and run automatically on a fixed schedule, usually daily or weekly. With a plain Docker Distribution registry without additional software, however, this logic must be implemented as a script that queries all tags of a repository via the registry HTTP API, applies the rules, and deletes surplus tags before the actual garbage collection run is started afterward.


# Harbor tag retention rule (example configuration)
rules:
  - repoMatches: "mironsoft/app"
    tagMatches: "sha-*"
    retention: "keep the most recently pushed 20 tags"
  - repoMatches: "mironsoft/app"
    tagMatches: "release-*"
    retention: "keep forever"

6. Lifecycle policies with cloud registries

Cloud registries such as AWS ECR, Google Artifact Registry, or Azure Container Registry come with their own lifecycle policy mechanisms that conceptually fulfill the same task as Harbor's retention policies, though configured in a provider-specific way. In ECR, for example, a lifecycle policy is defined as a JSON rule set with priorities that, say, automatically removes untagged images after a certain number of days and limits the number of stored images per tag prefix.

The decisive advantage of these managed solutions is that the actual garbage collection, meaning physically freeing storage after the deletion rules are applied, is carried out automatically by the cloud provider without any manual maintenance effort. With self-hosted registries, on the other hand, the operator must orchestrate both the retention policy and the actual garbage collection run themselves, usually via a cron job or as a periodic CI pipeline step separate from the actual application pipelines.


# Applying an AWS ECR lifecycle policy
aws ecr put-lifecycle-policy \
  --repository-name mironsoft/app \
  --lifecycle-policy-text '{
    "rules": [{
      "rulePriority": 1,
      "description": "Remove untagged images after 7 days",
      "selection": {"tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 7},
      "action": {"type": "expire"}
    }]
  }'

7. The special case of untagged manifests

A frequently overlooked detail is that overwriting a tag, for example when app:buildcache is pushed anew on every CI run as described in the article on BuildKit remote cache, does not automatically delete the previous manifest but leaves it in the registry as a so-called untagged or dangling manifest until it is explicitly removed or recognized as orphaned by garbage collection. With frequently changing cache tags or CI tags, this can accumulate a substantial amount of untagged manifests that are still referenced, or only actually removable after a GC run.

For this case, most registry APIs offer an endpoint to list all manifest digests of a repository and compare them against the set of currently assigned tags, so untagged manifests can be specifically identified and explicitly removed via a DELETE request before the next garbage collection run. This explicit cleanup speeds up the subsequent GC run, since fewer manifests need to be considered in the mark pass, and makes overall storage growth more predictable.

8. Monitoring storage growth

Without monitoring, storage growth usually goes unnoticed until the underlying volume actually fills up and pushes start failing, which in an active CI/CD pipeline can lead to a complete outage of all deploys. It therefore makes sense to regularly capture the storage occupied by the registry backend, for example via CloudWatch metrics with ECR, the storage endpoint in Harbor, or a custom script that measures the size of the underlying S3 bucket or filesystem and exports it as a metric to Prometheus or an equivalent system.

In addition to raw storage consumption, it is worth watching the number of tags per repository and their growth rate over time, since a sudden spike can indicate a faulty CI configuration, for example a pipeline that accidentally generates a new unique tag on every run instead of reusing a cache tag. An alert when a threshold for storage consumption or tag count is exceeded gives the operations team enough lead time to adjust retention policies before things become critical.

9. Best practices for sustainable registry operations

In practice, a combination of several measures works well: a clear tagging strategy as described in the previous article that distinguishes between release tags meant to be kept permanently and short-lived CI tags, an automated retention policy that removes short-lived tags after a defined period, a regular, ideally weekly garbage collection run during a maintenance window or with delete-marker support during live operation, and continuous monitoring of storage consumption with alerting on anomalies.

Anyone who plans these building blocks from the start, rather than retrofitting them only after a storage volume has already filled up, avoids not only unnecessary operating costs but also acute registry outages that in the worst case can block an entire team's deployment capability. The table below compares the key cleanup mechanisms and their respective role.

Mechanism What gets removed Trigger Typical tool
docker system prune Local, unused images/containers Manual on host/runner Docker CLI
Retention policy Surplus or stale tags Schedule or rule set Harbor, ECR, GitLab Registry
Garbage collection Physically orphaned blobs After tag deletion, periodic registry garbage-collect, cloud GC
Manual manifest cleanup Untagged, dangling manifests Ad hoc or before a GC run Registry HTTP API, custom script

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

Registry Garbage Collection: Key Takeaways

Core problem

docker system prune only acts locally; the registry's own storage growth is unaffected by it.

Two steps needed

First a retention policy decides which tags go, then garbage collection physically frees the storage.

Cloud vs. self-hosted

Cloud registries automate GC; self-hosted registries need their own cron jobs for it.

Monitoring matters

Watch storage consumption and tag counts to catch misconfigurations early.

11. FAQ: Registry Garbage Collection: Key Takeaways

1Is docker system prune enough to clean up a registry?
No, docker system prune only acts locally on the host or runner where it runs. The registry's own storage, usually an object store or filesystem on the registry server, remains completely unaffected.
2What happens when I delete a tag in the registry?
Usually only the mapping of the tag to the manifest is removed, not the underlying blobs. These are only physically freed by a separate garbage collection run, provided no other tag still references them.
3How does garbage collection work in Docker Distribution?
In two phases: a mark pass marks all manifest and layer digests still referenced by tags, followed by a sweep pass that physically deletes all unmarked, i.e. orphaned, blobs.
4Does the registry need to be offline during garbage collection?
With the classic Docker Distribution implementation, the registry should be read-only to avoid race conditions. Newer versions with delete-marker support allow garbage collection even during live operation.
5What is a retention policy?
A retention policy is a rule set that defines which tags to keep and which to delete, for example the last ten tags per repository or all tags older than ninety days except marked releases.
6Do cloud registries automate garbage collection?
Yes, with AWS ECR, Google Artifact Registry, or Azure Container Registry, the provider automatically handles the actual physical storage release once a lifecycle policy has determined which images to delete.
7What is an untagged or dangling manifest?
A manifest that is no longer referenced by any tag because a tag was overwritten, but that has not yet been removed by garbage collection. It continues to occupy storage until explicitly cleaned up or recognized by GC.
8How often should garbage collection run?
A weekly cadence has proven effective in practice for active CI/CD pipelines. With very high push frequency, a daily run can make sense to curb storage growth faster.
9How do I know if my registry has a storage problem?
By monitoring the storage occupied by the registry backend as well as the tag count per repository over time. A sudden, unexpected spike usually points to a faulty CI configuration.
10Can I protect release tags from garbage collection?
Yes, via retention policies with exception rules for certain tag prefixes such as release- or v*, which are kept permanently, while short-lived CI tags are deleted after a defined period.