GitLab Container Registry: Versioning Images, Cleaning Up, and Controlling Access
AI generated
CI/CD
.yml
GitLab · Container Registry · DevOps
GitLab Container Registry
Version, clean up, secure

The container registry built into GitLab saves you from running an external registry service, but it requires a deliberate tag strategy, automatic cleanup policies against runaway storage usage, and clearly defined access rights per project and group.

16 min read Container Registry Cleanup Policy Access Rights

1. A registry service that is already built into the project

Every GitLab project automatically comes with its own container registry as soon as the feature is enabled in the project settings. This saves teams from operating a separate registry service like a self-hosted Harbor instance or an external cloud registry subscription, along with the associated authentication, network configuration, and maintenance. The CI runner is already equipped with the necessary credentials via the predefined CI_REGISTRY variable, so an image can be built and pushed directly from the pipeline without manually storing additional secrets.

This tight integration is both the biggest advantage and the biggest risk: because pushing an image works so smoothly, hundreds or thousands of tags often accumulate over months, most of which are never needed again. Without a deliberate strategy for versioning and cleanup, the registry grows uncontrollably, which significantly hurts both storage costs and the ability to find a specific image quickly.

2. A tag strategy that ensures traceability

The most common beginner mistake is tagging every image only with latest. That works in the short term, but makes it impossible to trace which code state is actually running in a live container once multiple pushes have happened. A combination of several tags per build has proven effective instead: an immutable tag based on the commit SHA for exact traceability, a semantic version tag for releases, and optionally a latest tag exclusively for the newest state of the main branch.

It matters to treat the commit SHA tags as the actual source of truth and to understand semantic or latest tags merely as additional, movable pointers to them. This way, in an emergency, it is possible to trace exactly which image belongs to which commit, which is indispensable especially for rollbacks, when a quick switch back to a specific, known-working state is needed.


# .gitlab-ci.yml
build-image:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build
        -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
        -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
        .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
    - |
      if [ "$CI_COMMIT_TAG" ]; then
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
        docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
        docker push "$CI_REGISTRY_IMAGE:latest"
      fi

3. Cleanup policies: automatic cleanup instead of manual maintenance

Under Settings > Packages and registries > Clean up image tags, GitLab offers a built-in cleanup policy that runs periodically (the exact timing is scheduled internally by GitLab, not set by the user) and removes tags according to configurable rules. The central levers are a regex pattern for tags that should be kept, a second regex pattern for tags explicitly excluded from deletion, and a rule for how many of the newest tags per pattern are always retained.

A sensible baseline configuration keeps all tags that look like a semantic version (^v?\d+\.\d+\.\d+$) indefinitely, while commit SHA tags only keep the last twenty to thirty instances and all remaining, untagged, or older tags are deleted after a defined period. This combination ensures that release versions remain permanently traceable while storage usage stays bounded by the much more numerous intermediate build tags.


# Configure the cleanup policy via the API
curl --request PUT --header "PRIVATE-TOKEN: <token>" \
  "https://gitlab.example.com/api/v4/projects/123" \
  --data-urlencode "container_expiration_policy_attributes[enabled]=true" \
  --data-urlencode "container_expiration_policy_attributes[cadence]=1d" \
  --data-urlencode "container_expiration_policy_attributes[keep_n]=25" \
  --data-urlencode "container_expiration_policy_attributes[older_than]=30d" \
  --data-urlencode "container_expiration_policy_attributes[name_regex_keep]=^v?[0-9]+\.[0-9]+\.[0-9]+$" \
  --data-urlencode "container_expiration_policy_attributes[name_regex]=.*"

4. Manual and scheduled cleanup for special cases

The automatic cleanup policy covers the normal case, but some situations require targeted manual intervention, for example when a large batch of broken images from a faulty pipeline configuration needs to be removed before the next scheduled cleanup runs. The container registry API allows tags to be deleted selectively via script, which fits well into a one-off cleanup script or a dedicated, manually triggered CI job.

For recurring special cases, such as weekly removal of all images from deleted feature branches, a scheduled pipeline job (scheduled pipeline) is a good fit: it first queries all still-existing branches via the API and then deletes all registry tags that can no longer be associated with an active branch. This complements the general cleanup policy with targeted, branch-based cleanup logic.


# Targeted deletion of a single tag via the API
curl --request DELETE --header "PRIVATE-TOKEN: <token>" \
  "https://gitlab.example.com/api/v4/projects/123/registry/repositories/456/tags/feature-old-branch"

# Bulk deletion with a name filter (asynchronous, returns a job ID)
curl --request DELETE --header "PRIVATE-TOKEN: <token>" \
  "https://gitlab.example.com/api/v4/projects/123/registry/repositories/456/tags" \
  --data-urlencode "name_regex_delete=^feature-.*$" \
  --data-urlencode "keep_n=0"

5. Controlling access rights at the project level

By default, access to the container registry follows the project's general visibility: for a private project, users need at least Reporter access to pull images and at least Developer access to push. This coupling makes sense for most teams, but it can be fine-tuned under Settings > General > Visibility, project features, permissions, for example to restrict registry access to project members only, independent of the project's general visibility.

For CI/CD pipelines that need to access images from a different project (such as a central base-image repository), a deploy token with the read_registry scope is the right choice, rather than storing personal credentials in the pipeline configuration. Deploy tokens are project-bound, configurable with a limited lifetime, and can be revoked independently of any individual person's account, resulting in a much cleaner security model than shared personal tokens.

6. Access rights at the group level and across project boundaries

For organizations with several interrelated projects, for example a base-image project and several application projects built on top of it, a group-wide view is worthwhile. GitLab allows group deploy tokens to grant access to the registries of all projects within a group, which significantly reduces the management overhead of individual project tokens when a CI job in project B regularly needs to pull a base image from project A.

GitLab additionally supports the CI_DEPENDENCY_PROXY variables for the dependency proxy, which caches external registries like Docker Hub group-wide, reducing rate-limit problems with public registries while also enabling central control over which external base images may be used in the organization at all. This combination of an own registry and a dependency proxy covers most container-related access scenarios without needing an external service.


# Pull a base image via the dependency proxy instead of directly from Docker Hub
docker login -u "$CI_DEPENDENCY_PROXY_USER" \
  -p "$CI_DEPENDENCY_PROXY_PASSWORD" \
  "$CI_DEPENDENCY_PROXY_SERVER"

docker pull "$CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/library/node:20-alpine"

7. Watching storage usage before it becomes a problem

On GitLab.com, the container registry directly counts towards the namespace's storage quota, and even on self-hosted instances an unchecked, growing registry affects the storage capacity of the object storage backend. Under Settings > Usage Quotas > Storage, the current registry consumption can be viewed separately from repository and artifact storage, which shows early on whether the cleanup policy is actually taking effect or whether tags are accumulating unnoticed despite enabled rules, for example because a regex pattern accidentally excludes too many tags from deletion.

A regular look at this metric, combined with a brief spot check of the actually present tags via the registry UI, usually surfaces configuration mistakes within a few weeks instead of storage usage only becoming apparent after months of uncontrolled growth. Especially with several parallel active projects each with its own registry, a monthly collective check across all namespaces is worthwhile to identify outliers early.

8. Optimizing image size before cleanup even becomes necessary

The most effective measure against a sprawling registry starts before cleanup: smaller images cause less storage usage per tag and get pushed and pulled faster. Multi-stage builds strictly separate the build environment, with its compiler, Composer cache, and dev dependencies, from the lean runtime image, so that only the actually required artifacts end up in the final layer. For a PHP project, this typically means running composer install --no-dev only in the last stage and selectively copying the vendor folder from an earlier build stage.

It is also worth deliberately ordering the layers in the Dockerfile: rarely changing layers such as the operating system base image and system packages belong at the top, frequently changing application code at the bottom, so the Docker build cache can reuse as many layers as possible. This not only reduces build time in the pipeline but also keeps the number of distinct, actually stored layer combinations in the registry smaller.

9. Common mistakes when operating the container registry

A recurring mistake is a too-aggressive regex pattern in the cleanup policy that accidentally also matches semantic version tags, deleting release images that should actually be kept permanently. Before activating a new cleanup rule in production, it is worth running a test against a small sample of tags, or at least carefully checking the regex pattern against the actually existing tag list.

A second common mistake is using personal access tokens instead of deploy tokens for automated access. If the responsible person leaves the team or their account is deactivated, access unexpectedly fails for all pipelines relying on it. The table below summarizes the key mechanisms for versioning, cleanup, and access control.

Mechanism Purpose Recommended configuration Location in GitLab
Commit SHA tag Exact traceability of every build Always set in addition to other tags CI job (docker build/push)
Cleanup policy Automatically removing old tags Keep semantic versions, bound SHA tags Settings > Packages and registries
Deploy token (read_registry) Pipeline access without a personal account Use for all automated pulls Settings > Repository > Deploy tokens
Group deploy token Access to several project registries For a central base-image project Group > Settings > Deploy tokens
Dependency proxy Caching external images, rate-limit protection For all Docker Hub base images Group > Packages > Dependency Proxy

Mironsoft

CI/CD pipelines, zero-downtime deployments and release automation

Deployments that run without downtime and without the nail-biting?

We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.

Pipeline Review

Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.

Zero-Downtime Deployment

Building symlink releases, health checks and rollback strategies for Magento stores.

CI/CD Automation

Connecting tests, security scans and deployments into one reliable pipeline.

10. Summary

Container Registry: The Essentials at a Glance

Tag strategy

Commit SHA as the source of truth, semantic versions and latest as movable pointers.

Cleanup policy

Automatic cleanup with regex exceptions for release versions.

Deploy tokens

Project-bound, revocable access instead of personal credentials.

Dependency proxy

Cache external base images group-wide instead of pulling directly from Docker Hub.

11. FAQ: Container Registry: The Essentials at a Glance

1How often does the cleanup policy run by default?
The exact execution frequency is scheduled internally by GitLab and is not precisely controllable by the user, but can be set via the 'cadence' field to daily, weekly, every two weeks, or monthly.
2Can I restore a deleted image?
No, deleting a tag in the container registry cannot be undone via the GitLab interface. If a backup mechanism exists for the object storage backend, restoration is theoretically possible but is not part of the standard functionality.
3What is the difference between a deploy token and a personal access token for the registry?
A deploy token is project-bound and independent of any individual user account, while a personal access token is tied to a specific person and becomes invalid if they leave or their account is deactivated.
4Why are some tags not deleted despite an active cleanup policy?
The most common cause is a too-broad name_regex_keep pattern or a too-high keep_n number that retains more tags than intended. A targeted test run of the regex pattern against the existing tag list usually uncovers such mistakes quickly.
5Can I disable the container registry entirely if I don't use it?
Yes, under Settings > General > Visibility, project features, permissions the registry feature can be fully disabled per project, which also removes it from the project navigation.
6How does the dependency proxy differ from the container registry?
The container registry stores your own, self-built images, while the dependency proxy caches external images from registries like Docker Hub to avoid rate limits and reduce repeated external downloads.
7Does the dependency proxy cache count towards the project's storage quota?
Yes, images cached via the dependency proxy also count towards the group's storage usage and should be factored in when monitoring overall storage.
8Can I combine several cleanup rules for different tag patterns?
The built-in cleanup policy only allows one rule set per project with one keep and one delete pattern. For more complex, multi-stage rules, a custom script via the registry API is necessary.
9How do I check which image is currently running in production?
Most reliably via the commit SHA tag, which should be logged during deployment, for example as an environment variable in the running container or as an entry in the pipeline's deployment log.
10Should I use latest tags in production?
No, latest tags are a movable pointer and are not suited for reproducible deployments. Production environments should always reference an immutable tag such as the commit SHA or a fixed semantic version.