Registry Cleanup Policies
Automatic Cleanup
Retention Policies
Storage Management
A complete guide to GitLab Container Registry cleanup policies covering automatic cleanup, retention rules, storage management, and best practices for controlling image storage costs.
Cleanup
Retention
Storage Management
GitLab
Why Registry Cleanup Matters
GitLab Container Registry storage grows relentlessly. Every pipeline that pushes an image adds storage, and without cleanup, costs spiral quickly. Consider a moderately active project: if you push a 500MB image on every commit across 20 commits a day, that's 10GB of storage added daily — 300GB per month — most of which is never pulled again. Multiplied across an organization's projects, the cost becomes enormous.
But cleanup isn't just about cost. It's also about performance (registry operations slow as the registry grows), compliance (regulations require data retention limits), security (old images with known vulnerabilities persist and could be pulled accidentally), and clarity (a clean registry is easier to audit). For all these reasons, automatic cleanup is essential in any serious GitLab deployment.
GitLab provides cleanup policies that automatically remove old images based on rules you define. You specify which images are eligible for deletion (by age, by tag pattern, by name regex) and which should always be kept. GitLab then runs the cleanup daily, deleting images that match the deletion criteria and don't match the keep criteria. This is a fire-and-forget mechanism — once configured, you don't need to think about it again.
Key Concept: Cleanup policies are rule-based, not heuristic. You define exactly what to delete and what to keep. This gives you precise control but also means you must think carefully about your rules — a misconfigured policy can delete images you still need.
How Cleanup Policies Work
Cleanup policies operate on tags, not on individual image layers. When you push an image with a tag, that tag becomes a candidate for cleanup. When a tag is deleted by the cleanup policy, all the image layers that are only referenced by that tag are also deleted (via garbage collection). This means storage is reclaimed, not just metadata.
Every cleanup policy has several parameters that determine its behavior. The enabled flag turns the policy on or off. The cadence determines how often it runs (daily, every few days, or monthly). The older_than parameter specifies the age threshold — images older than this will be considered for deletion. The keep_n parameter specifies how many images to keep regardless of age. The name_regex_delete and name_regex_keep parameters define patterns for inclusion and exclusion.
The logic is: an image tag is deleted if it matches name_regex_delete, is older than older_than, is beyond the keep_n most recent tags, AND does not match name_regex_keep. The combination of these conditions gives fine-grained control. Most teams end up with a simple policy: keep release tags forever, keep the last N images per branch, and delete anything older than 30 or 90 days.
# Cleanup policy logic (pseudocode)
for each image tag in the project:
if not policy.enabled:
skip
if tag matches name_regex_keep:
keep # Never delete
if tag matches name_regex_delete:
if tag_age > policy.older_than:
if tag_position > policy.keep_n:
delete
else:
keep # Not old enough
# Example: Delete images older than 30 days,
# keep the 10 most recent, but never delete "release-*"
# (simplified)
policy:
enabled: true
cadence: "1d"
older_than: "30d"
keep_n: 10
name_regex_delete: ".*" # Delete all
name_regex_keep: "release-.*" # Except releases
# Result:
# - release-1.0.0 → kept (matches name_regex_keep)
# - main-abc123 (25 days old, in the 10 most recent) → kept
# - main-def456 (35 days old, beyond 10 most recent) → deleted
# - dev-xyz789 (5 days old, in the 10 most recent) → kept
# - dev-uvw012 (35 days old, beyond 10 most recent) → deleted
How Policies Run:
- Policies run on a schedule (daily, weekly, or monthly)
- Deletion is permanent — deleted images cannot be recovered
- Policies can be tested with a dry run (Preview before saving)
- Only GitLab Premium and above support automatic cleanup
- Free tier users can clean up manually via UI or API
Policy Parameters
Each parameter of a cleanup policy controls a different aspect of its behavior. Understanding each one is important — they interact in subtle ways, and a small change can have a large effect.
| Parameter |
Description |
Common Values |
| enabled |
Turn the policy on or off |
true / false |
| cadence |
How often the policy runs |
1d, 7d, 14d, 1month |
| older_than |
Delete images older than this age |
7d, 30d, 90d |
| keep_n |
Keep the N most recent images |
5, 10, 20, 50 |
| name_regex_delete |
Which tags to consider for deletion |
.*, dev-.* |
| name_regex_keep |
Which tags to always keep |
release-.*, v\d+\.\d+\.\d+ |
| older_than_days |
Age threshold in days (numeric) |
30, 90 |
| keep_n_matches |
Regex matches that are always kept |
Combined with keep_n |
# Understanding each parameter in detail
# 1. enabled
# Just toggle — true means cleanup runs, false means it's paused
# 2. cadence
# How often cleanup runs:
# 1d (daily), 7d (weekly), 14d (every 2 weeks), 1month (monthly)
# Daily is best for active projects, monthly is fine for quiet ones
# 3. older_than
# Delete images older than N days
# Common values: 7d (feature branches), 30d (dev), 90d (archival)
# Shorter = more aggressive cleanup, more cost savings
# Longer = more rollback capability, more storage cost
# 4. keep_n
# Keep at least N images regardless of age
# Common values: 5-20 for feature branches, 3-5 for releases
# Ensures recent images are always available even if older_than
# would delete them
# 5. name_regex_delete
# Only consider tags matching this regex for deletion
# Usually ".*" to consider all tags
# Could be "dev-.*" to only clean up dev tags
# 6. name_regex_keep
# Never delete tags matching this regex
# Use for release tags, tagged releases, etc.
# Example: "release-.*" or "v\d+\.\d+\.\d+"
# Example policies:
# Policy 1: Aggressive cleanup (cost-optimized)
enabled: true
cadence: "1d"
older_than: "7d"
keep_n: 5
name_regex_delete: ".*"
name_regex_keep: "release-.*"
# Result: Keep only release images older than 7 days.
# Delete all others after 7 days, keep max 5.
# Policy 2: Conservative cleanup (rollback-friendly)
enabled: true
cadence: "7d"
older_than: "90d"
keep_n: 20
name_regex_delete: ".*"
name_regex_keep: "release-.*|v\d+\.\d+\.\d+"
# Result: Keep last 20 images per branch, never delete
# anything less than 90 days old. Very safe.
# Policy 3: Feature branch cleanup
enabled: true
cadence: "1d"
older_than: "7d"
keep_n: 3
name_regex_delete: "feature-.*"
name_regex_keep: "feature-keep-.*"
# Result: Delete feature branch images after 7 days, except
# ones matching "feature-keep-*". Keeps 3 most recent.
# Policy 4: Version-based retention
enabled: true
cadence: "7d"
older_than: "30d"
keep_n: 5
name_regex_delete: "v.*"
name_regex_keep: "v\d+\.\d+\.\d+$"
# Result: Delete version tags older than 30 days, but
# always keep semantic versions (v1.2.3). Keeps 5 recent.
# Dry run (preview without deleting)
# GitLab UI: Preview before saving
# Shows what would be deleted
Choosing Parameters:
- Active projects: Daily cadence, 7-30 day retention
- Quiet projects: Weekly cadence, 30-90 day retention
- Release images: Always keep via
name_regex_keep
- Feature branches: Aggressive (7 days, keep 3)
- Default branches: Conservative (30-90 days, keep 10)
Configuring Cleanup Policies
Cleanup policies can be configured through the GitLab UI, the API, or Terraform. The UI is the easiest for one-off configuration; the API is essential for automating across many projects; Terraform is best for infrastructure-as-code deployments.
Regardless of method, the process is: enable cleanup, choose a cadence, set an age threshold and keep count, and define name patterns. Then preview the policy — GitLab shows you which images would be deleted, so you can verify before committing. Once satisfied, save and enable the policy, and it will run on its schedule.
# Configure via GitLab UI
# Project → Settings → Packages and Registries → Cleanup policies
# 1. Enable cleanup policies
# 2. Set cadence: 1d, 7d, 14d, or 1month
# 3. Set "Keep the most recent" (keep_n): 5-50
# 4. Set "Remove images older than" (older_than): 1d-90d
# 5. Set "Remove tags matching" (name_regex_delete): regex
# 6. Set "Keep tags matching" (name_regex_keep): regex
# 7. Preview → Save
# Configure via API (gitlab.com)
curl --request PUT \
--header "PRIVATE-TOKEN:
" \
--header "Content-Type: application/json" \
--data '{
"container_expiration_policy_attributes": {
"cadence": "1d",
"enabled": true,
"keep_n": 10,
"older_than": "30d",
"name_regex_delete": ".*",
"name_regex_keep": "release-.*"
}
}' \
"https://gitlab.com/api/v4/projects/"
# Check current policy
curl --header "PRIVATE-TOKEN: " \
"https://gitlab.com/api/v4/projects/" \
| jq '.container_expiration_policy'
# Preview policy (see what would be deleted)
curl --header "PRIVATE-TOKEN: " \
"https://gitlab.com/api/v4/projects//registry/repositories"
# Configure via Terraform
resource "gitlab_project" "example" {
name = "my-project"
# ... other configuration ...
container_expiration_policy {
cadence = "1d"
enabled = true
keep_n = 10
older_than = "30d"
name_regex_delete = ".*"
name_regex_keep = "release-.*"
}
}
# Configure via GitLab CI (only disable, not set)
# You can disable cleanup via CI, but not configure it
# project-expiration: script in CI can call the API
# Best practice: automate via Terraform
# Ensure all projects have consistent cleanup policies
# by defining them in your Terraform configuration
# Dry run: preview before saving
# GitLab shows the images that would be deleted
# Review carefully before enabling the policy
# Apply across all projects (bulk)
for project_id in $(seq 1 100); do
curl --request PUT \
--header "PRIVATE-TOKEN: " \
--data '{"container_expiration_policy_attributes":{...}}' \
"https://gitlab.com/api/v4/projects/${project_id}"
done
# Disable cleanup
curl --request PUT \
--header "PRIVATE-TOKEN: " \
--data '{"container_expiration_policy_attributes":{"enabled":false}}' \
"https://gitlab.com/api/v4/projects/"
Important:
- Cleanup policies run automatically once configured
- Deletion is permanent — no recovery
- Always preview before saving
- Test in a non-production project first
- Consider dry-running for a week before enabling
- Be careful with
keep_n — setting it too low can delete images you need
Retention Strategies
The right retention strategy depends on your workflow. Different types of images have different lifecycle needs. A release image should be kept forever; a feature branch image should be cleaned aggressively; a nightly build might be kept for a week.
Segmenting images by tag pattern enables different retention rules for different categories. Use a consistent tagging convention (e.g., release-v1.2.3 for releases, feature-xyz-abc123 for feature branches, nightly-20240101 for nightlies), then write cleanup rules that match each category. This gives you fine-grained control without complexity.
Release Images
Keep forever. Tagged with semantic versions (v1.2.3). Use name_regex_keep to protect them.
Main Branch Images
Keep recent for rollback. Tag with commit SHA. Keep 10-20 images, delete older than 30 days.
Feature Branch Images
Keep short-lived. Tag with branch name + SHA. Delete after 7-14 days, keep only 3-5 recent.
Nightly Builds
Keep for trend analysis. Tag with date. Keep 7-30 days, delete after a week.
Test Images
Delete aggressively. Tag with test-*. Delete after 1-3 days.
Archival Images
Keep for compliance. Tag with archive-*. Never delete automatically.
# Retention strategy: multiple policies via tagging
# Tagging convention:
# - release-v1.2.3 (releases - keep forever)
# - main-abc123 (main branch - keep 10, delete after 30d)
# - feature-xyz-abc123 (feature branch - delete after 7d, keep 3)
# - nightly-20240101 (nightly - delete after 7d)
# - test-abc123 (test - delete after 1d)
# Cleanup policy (covers all categories):
enabled: true
cadence: "1d"
older_than: "30d"
keep_n: 10
name_regex_delete: ".*"
name_regex_keep: "release-.*|archive-.*"
# This policy:
# - Keeps all "release-*" and "archive-*" tags (never deleted)
# - Keeps the 10 most recent tags per branch
# - Deletes tags older than 30 days, except the 10 most recent
# Additional cleanup for feature branches:
# (If needed, use a second policy or more aggressive settings)
# Simpler approach: single policy
enabled: true
cadence: "1d"
older_than: "30d"
keep_n: 10
name_regex_delete: ".*"
name_regex_keep: "(release|archive)-.*"
# This is often sufficient for most teams
# For monorepos with many services:
# Each service has its own image
# Use separate cleanup policies per service (if possible)
# Or use tag patterns to differentiate: service-a-*, service-b-*
# Retention strategy for compliance:
# - Keep everything for 90 days (regulatory minimum)
# - After 90 days, keep only releases
# - Never delete releases (audit trail)
# Recommended baseline:
# cadence: 1d (daily)
# older_than: 30d (30 days)
# keep_n: 10 (last 10 per branch)
# name_regex_keep: "release-.*|v\d+\.\d+\.\d+"
Retention Strategy Summary:
- Use tag patterns to categorize images (release, feature, test)
- Protect releases with
name_regex_keep
- Delete feature and test images aggressively
- Keep enough history for rollback (10-20 per branch)
- Test your policy with a preview before enabling
- Monitor storage after enabling to verify savings
Storage Management
Cleanup policies remove image tags, but they don't immediately free disk space. Deleting a tag marks the underlying blobs as unreferenced; a separate garbage collection process (usually run by the registry administrator) actually deletes them. On GitLab.com, this happens automatically. On self-hosted GitLab, you may need to run garbage collection manually.
Beyond cleanup, storage management includes monitoring usage, identifying large images, and optimizing image sizes. A 2GB image is 4x the cost of a 500MB image and 4x slower to pull. Building smaller images (smaller base images, multi-stage builds, layer optimization) is often more impactful than cleanup alone.
For self-hosted GitLab, storage is on your infrastructure (object storage or filesystem), and you're responsible for its capacity. For GitLab.com, storage is metered and billed — exceeding your plan's limit requires purchasing additional storage or reducing usage through cleanup and optimization.
# Monitoring storage usage
# GitLab UI
# Project → Packages and Registries → Container Registry
# Shows:
# - Total size of all images
# - Size per tag
# - Repository count
# API
curl --header "PRIVATE-TOKEN:
" \
"https://gitlab.com/api/v4/projects//registry/repositories"
# Returns: repository sizes, tag counts, etc.
# Group-level storage usage
# GitLab UI: Group → Settings → Usage Quotas
# Shows: Total storage used, breakdown by project
# Storage optimization tips:
# 1. Use smaller base images
# BAD: FROM ubuntu:20.04 (72MB)
# GOOD: FROM alpine:3.18 (5MB)
# Better: FROM scratch (0MB, if possible)
# 2. Multi-stage builds
# BAD: Single stage builds include build tools
# GOOD: Multi-stage builds only include runtime
FROM node:18 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# 3. Combine RUN commands to reduce layers
# BAD: Multiple RUN commands
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# GOOD: Single RUN command
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
# 4. Use .dockerignore
# Exclude node_modules, .git, tests, etc.
# 5. Squash layers (if appropriate)
docker build --squash .
# Storage cleanup for self-hosted GitLab
# Run garbage collection
sudo gitlab-ctl registry-garbage-collect
# Delete untagged manifests
sudo gitlab-ctl registry-garbage-collect -m
# Schedule GC (cron)
# Run weekly to reclaim disk space
0 3 * * 0 root /usr/bin/gitlab-ctl registry-garbage-collect -m
# Check storage usage (self-hosted)
du -sh /var/opt/gitlab/gitlab-rails/shared/registry/
# Storage analytics
# Use the API to track storage over time
# Set up alerts if storage exceeds threshold
# Cost optimization:
# 1. Aggressive cleanup policies
# 2. Smaller images (Alpine, distroless)
# 3. Multi-stage builds
# 4. Object storage (cheaper than block storage)
# 5. Monitor usage and alert on anomalies
Storage Optimization:
- Cleanup policies free tag metadata; GC frees actual blobs
- Self-hosted: run GC weekly
- GitLab.com: GC is automatic
- Smaller images = lower cost + faster pulls
- Monitor storage monthly; adjust policy if growing
Troubleshooting Cleanup Policies
# Cleanup policy not running
# 1. Check it's enabled
# Project → Settings → Packages and Registries → Cleanup policies
# 2. Check the schedule (cadence)
# 3. Check if premium tier (automatic cleanup requires premium)
# Images not being deleted
# 1. Check name_regex_delete (must match tag names)
# 2. Check name_regex_keep (must NOT match)
# 3. Check older_than (images must be older)
# 4. Check keep_n (must be beyond keep_n most recent)
# 5. Check cadence (has it run yet?)
# Preview to see what would be deleted
# Project → Settings → Packages and Registries → Cleanup policies
# Click "Preview" → Shows tags that would be deleted
# Deleted images still show in registry
# - Blobs are deleted by garbage collection
# - On GitLab.com, wait a few days
# - On self-hosted, run GC manually:
sudo gitlab-ctl registry-garbage-collect -m
# Storage not decreasing after cleanup
# - Cleanup deletes tags; GC deletes blobs
# - Wait for GC cycle (or run manually)
# - Check actual storage usage in UI
# - Check group quota usage
# Accidentally deleted images
# - No recovery from cleanup deletion
# - Rebuild the image if you have the source
# - If using registry backups, restore from backup
# - Add more images to keep_n for next time
# Check cleanup history
# Project → Settings → Packages and Registries → Cleanup policies
# Shows last run and results
# Monitor for excessive storage
curl --header "PRIVATE-TOKEN:
" \
"https://gitlab.com/api/v4/projects//registry/repositories" \
| jq '.[] | {path: .path, size: .size, tags_count: .tags_count}'
# Force cleanup (self-hosted)
sudo gitlab-rake gitlab:cleanup:orphan_registry_blobs
sudo gitlab-ctl registry-garbage-collect -m
# Debug cleanup via logs
sudo gitlab-ctl tail registry
sudo gitlab-ctl tail sidekiq | grep ContainerExpiration
# Prevent accidental deletion
# - Use "Preview" before saving policy
# - Enable protection for release tags
# - Use "keep_n" generously (10-20)
# - Add regex_keep for important tags
# - Test in staging project first
Common Mistakes:
- Setting
keep_n too low (deletes needed images)
- Not using
name_regex_keep for releases
- Setting
older_than too short (deletes recent images)
- Enabling without previewing first (accidental deletion)
- Forgetting garbage collection on self-hosted
- Not monitoring storage after cleanup
Best Practices Summary
Consistent Tagging
Use a consistent tag convention so cleanup rules are simple and reliable.
Protect Releases
Always use name_regex_keep for release tags. They're your audit trail.
Preview Before Saving
Always use the preview feature to see what would be deleted before enabling.
Monitor Storage
Track storage monthly. Adjust policies if usage grows unexpectedly.
Optimize Images
Cleanup + image optimization = maximum savings. Smaller images save more than aggressive cleanup.
Automate via Terraform
Define cleanup policies in code so all projects have consistent policies.
Document Policies
Document retention policies for audit and compliance. Why did you keep what you kept?
Run GC Weekly
On self-hosted, run garbage collection weekly to reclaim disk space.
The Bottom Line:
- Cleanup policies are mandatory for production registries
- Configure them once, monitor them forever
- Start conservative; tighten after testing
- Combine with image optimization for maximum savings
- Document the policy for compliance and audits
Frequently Asked Questions
Are deleted images recoverable?
No. Cleanup deletion is permanent. The image tags are removed from the registry, and the underlying blobs are garbage collected. Make sure your policy is correct before enabling.
Does cleanup free disk space immediately?
No. Cleanup removes tag metadata; a separate garbage collection process removes blobs. On GitLab.com this is automatic; on self-hosted, run gitlab-ctl registry-garbage-collect.
What's the difference between cleanup and garbage collection?
Cleanup removes tags (metadata) according to policies. Garbage collection removes blobs that are no longer referenced by any tag. Both are needed to fully reclaim storage.
Can I protect certain images from cleanup?
Yes, use name_regex_keep to always keep tags matching a pattern. Use this for release tags, archived builds, or anything you need permanently.
How do I choose older_than and keep_n?
Balance rollback capability vs storage cost. Common: older_than: 30d, keep_n: 10. Aggressive: 7d/5. Conservative: 90d/20.
Is cleanup available on the free tier?
Automatic cleanup is a Premium feature. On the free tier, you can clean up manually through the UI or API, but it's not automated.
What's the best cleanup cadence?
Daily (1d) for active projects to keep storage under control. Weekly (7d) is fine for quieter projects. Monthly (1month) is minimal but may allow large storage spikes.
Can I have different policies for different projects?
Yes, cleanup policies are per-project. Each project can have its own policy. For consistency, define them in Terraform so all projects use the same rules.
Cleanup policies are essential for keeping registry costs under control. Configure them once, monitor monthly, and your storage will stay manageable.