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.
Table of Contents
- 1. A registry service that is already built into the project
- 2. A tag strategy that ensures traceability
- 3. Cleanup policies: automatic cleanup instead of manual maintenance
- 4. Manual and scheduled cleanup for special cases
- 5. Controlling access rights at the project level
- 6. Access rights at the group level and across project boundaries
- 7. Watching storage usage before it becomes a problem
- 8. Optimizing image size before cleanup even becomes necessary
- 9. Common mistakes when operating the container registry
- 10. Summary
- 11. FAQ
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.