Configure GitLab container registry cleanup policies and storage management

Intermediate 25 min Apr 24, 2026 16 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up automated cleanup policies for GitLab container registry to manage storage costs and remove unused container images. This tutorial covers UI configuration, API automation, and monitoring for production environments.

Prerequisites

  • GitLab instance with container registry enabled
  • GitLab admin or maintainer access
  • Personal access token with API scope
  • Basic knowledge of container registries and GitLab CI/CD

What this solves

GitLab container registries can quickly consume significant storage space as Docker images accumulate from CI/CD pipelines and development workflows. Without proper cleanup policies, old and unused images pile up, leading to increased storage costs and slower registry operations. This tutorial shows you how to configure automated cleanup policies that remove unused images based on age, tags, and retention rules while preserving production images.

Understanding GitLab container registry cleanup policies

GitLab cleanup policies work at the project level and can target specific image repositories within your container registry. The cleanup system evaluates images based on several criteria including tag patterns, image age, and the number of images to retain.

Cleanup policies support different rule types that determine which images get removed. Tag-based rules match specific tag patterns like feature- or dev-. Age-based rules remove images older than a specified number of days. Count-based rules keep only the most recent N images that match the criteria.

Note: Cleanup policies only affect untagged manifests and images that match your specific criteria. Images referenced by currently deployed applications or marked with protection rules are preserved.

Step-by-step configuration

Access project cleanup policy settings

Navigate to your GitLab project and access the container registry cleanup configuration through the project settings menu.

In your GitLab project, go to Settings > Packages and registries. Scroll down to the Cleanup policy for tags section. This is where you'll configure all cleanup rules for your project's container registry.

Configure basic cleanup policy

Set up a basic cleanup policy that removes old development and feature branch images while preserving production tags.

Enable the cleanup policy by toggling the Cleanup policy switch. Set the following basic configuration:

  • Expiration interval: 7 days (removes images older than 7 days that match other criteria)
  • Expiration schedule: Every day at 06:00 UTC
  • Number of tags to retain: 10 (keeps the 10 most recent tags that match patterns)
  • Tags with names matching this regex will expire: .* (matches all tags)
  • Tags with names matching this regex will be preserved: ^(main|master|production|release-.*|v\d+\.\d+\.\d+)$

This configuration removes images older than 7 days while always preserving main, master, production, release, and semantic version tags.

Add advanced cleanup rules

Create more specific rules for different types of images in your registry to optimize storage usage.

Click Add another rule to create additional cleanup policies. Configure separate rules for different image categories:

Rule for feature branches:

  • Tags matching: ^feature-.*
  • Expiration interval: 3 days
  • Keep N tags: 5

Rule for development images:

  • Tags matching: ^(dev|develop|test)-.*
  • Expiration interval: 5 days
  • Keep N tags: 3

Rule for pull request images:

  • Tags matching: ^pr-\d+
  • Expiration interval: 1 day
  • Keep N tags: 2

Configure cleanup via GitLab API

Use the GitLab API to programmatically manage cleanup policies across multiple projects for consistent configuration.

First, create a personal access token with api scope in GitLab under User Settings > Access Tokens. Then use the API to configure cleanup policies:

#!/bin/bash
GITLAB_TOKEN="your_personal_access_token"
GITLAB_URL="https://gitlab.example.com"
PROJECT_ID="123"

Get current cleanup policy

curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "$GITLAB_URL/api/v4/projects/$PROJECT_ID/registry/repositories"

Update cleanup policy

curl --request PUT \ --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "container_expiration_policy_attributes": { "enabled": true, "cadence": "1d", "older_than": "7d", "keep_n": 10, "name_regex": ".*", "name_regex_keep": "^(main|master|production|release-.*|v[0-9]+\.[0-9]+\.[0-9]+)$" } }' \ "$GITLAB_URL/api/v4/projects/$PROJECT_ID"

Set up CLI-based cleanup automation

Create a script that manages cleanup policies across multiple projects using GitLab's API for bulk operations.

Create a cleanup management script for consistent policy deployment:

#!/bin/bash

set -euo pipefail

Configuration

GITLAB_TOKEN="${GITLAB_TOKEN:-}" GITLAB_URL="${GITLAB_URL:-https://gitlab.example.com}" CONFIG_FILE="${CONFIG_FILE:-/opt/cleanup-config.json}" if [[ -z "$GITLAB_TOKEN" ]]; then echo "Error: GITLAB_TOKEN environment variable is required" exit 1 fi

Function to apply cleanup policy to a project

apply_cleanup_policy() { local project_id="$1" local policy_config="$2" echo "Applying cleanup policy to project $project_id..." response=$(curl --silent --request PUT \ --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ --header "Content-Type: application/json" \ --data "$policy_config" \ "$GITLAB_URL/api/v4/projects/$project_id" || echo "") if [[ -n "$response" ]]; then echo "✓ Policy applied to project $project_id" else echo "✗ Failed to apply policy to project $project_id" fi }

Function to get registry storage usage

get_registry_usage() { local project_id="$1" curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "$GITLAB_URL/api/v4/projects/$project_id/registry/repositories" | \ jq -r '.[] | "Repository: \(.path) | Size: \(.size_bytes // "unknown") bytes"' }

Read projects and apply policies

if [[ -f "$CONFIG_FILE" ]]; then jq -r '.projects[] | "\(.id) \(.cleanup_policy | tostring)"' "$CONFIG_FILE" | \ while read -r project_id policy_json; do apply_cleanup_policy "$project_id" "$policy_json" echo "Current registry usage for project $project_id:" get_registry_usage "$project_id" echo "---" done else echo "Config file $CONFIG_FILE not found" exit 1 fi

Create the configuration file for your cleanup policies:

{
  "projects": [
    {
      "id": 123,
      "name": "web-application",
      "cleanup_policy": {
        "container_expiration_policy_attributes": {
          "enabled": true,
          "cadence": "1d",
          "older_than": "7d",
          "keep_n": 10,
          "name_regex": ".*",
          "name_regex_keep": "^(main|master|production|release-.*|v[0-9]+\\.[0-9]+\\.[0-9]+)$"
        }
      }
    },
    {
      "id": 456,
      "name": "api-service",
      "cleanup_policy": {
        "container_expiration_policy_attributes": {
          "enabled": true,
          "cadence": "1d",
          "older_than": "3d",
          "keep_n": 5,
          "name_regex": "^(feature-.|dev-.|test-.*)$",
          "name_regex_keep": "^(main|production)$"
        }
      }
    }
  ]
}

Make the script executable and run it:

sudo chmod +x /opt/gitlab-registry-cleanup.sh
export GITLAB_TOKEN="your_token_here"
export GITLAB_URL="https://gitlab.example.com"
/opt/gitlab-registry-cleanup.sh

Monitor registry storage usage

Set up monitoring to track storage usage and cleanup effectiveness across your GitLab container registries.

Create a monitoring script that tracks registry metrics:

#!/bin/bash

set -euo pipefail

GITLAB_TOKEN="${GITLAB_TOKEN:-}"
GITLAB_URL="${GITLAB_URL:-https://gitlab.example.com}"
METRICS_FILE="${METRICS_FILE:-/var/log/registry-metrics.log}"

Function to get project registry statistics

get_project_registry_stats() { local project_id="$1" local project_name="$2" # Get project details project_info=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "$GITLAB_URL/api/v4/projects/$project_id") # Get registry repositories repositories=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "$GITLAB_URL/api/v4/projects/$project_id/registry/repositories") echo "=== Project: $project_name (ID: $project_id) ===" echo "Last activity: $(echo "$project_info" | jq -r '.last_activity_at')" total_size=0 repo_count=0 echo "$repositories" | jq -c '.[]' | while read -r repo; do repo_id=$(echo "$repo" | jq -r '.id') repo_path=$(echo "$repo" | jq -r '.path') repo_size=$(echo "$repo" | jq -r '.size_bytes // 0') # Get tags for this repository tags=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "$GITLAB_URL/api/v4/projects/$project_id/registry/repositories/$repo_id/tags" || echo '[]') tag_count=$(echo "$tags" | jq 'length') echo "Repository: $repo_path" echo " Size: $repo_size bytes ($(numfmt --to=iec "$repo_size"))" echo " Tags: $tag_count" if [[ "$tag_count" -gt 0 ]]; then echo " Recent tags:" echo "$tags" | jq -r '.[:5][] | " - \(.name) (\(.created_at))"' fi echo "" total_size=$((total_size + repo_size)) repo_count=$((repo_count + 1)) done echo "Total repositories: $repo_count" echo "Total size: $total_size bytes ($(numfmt --to=iec "$total_size"))" # Log metrics in structured format timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "$timestamp,$project_id,$project_name,$repo_count,$total_size" >> "$METRICS_FILE" }

Function to generate summary report

generate_summary() { if [[ -f "$METRICS_FILE" ]]; then echo "=== Storage Usage Summary ===" echo "Date,Project_ID,Project_Name,Repositories,Total_Size_Bytes" | head -1 tail -10 "$METRICS_FILE" echo "" echo "Top 5 projects by storage usage:" tail -50 "$METRICS_FILE" | sort -t',' -k5 -nr | head -5 | \ while IFS=',' read -r date proj_id proj_name repo_count size_bytes; do size_human=$(numfmt --to=iec "$size_bytes") echo " $proj_name (ID: $proj_id): $size_human ($repo_count repos)" done fi }

Main execution

if [[ $# -gt 0 && "$1" == "--summary" ]]; then generate_summary exit 0 fi

Create metrics file with header if it doesn't exist

if [[ ! -f "$METRICS_FILE" ]]; then echo "timestamp,project_id,project_name,repository_count,total_size_bytes" > "$METRICS_FILE" fi

Monitor specific projects (add your project IDs here)

projects=( "123:web-application" "456:api-service" "789:mobile-app" ) for project in "${projects[@]}"; do IFS=':' read -r project_id project_name <<< "$project" get_project_registry_stats "$project_id" "$project_name" echo "" done generate_summary

Set up automated cleanup scheduling

Configure systemd timers to run cleanup monitoring and policy enforcement on a regular schedule.

Create a systemd service for registry monitoring:

[Unit]
Description=GitLab Container Registry Monitoring
After=network.target

[Service]
Type=oneshot
User=gitlab-registry
Group=gitlab-registry
Environment=GITLAB_TOKEN=your_token_here
Environment=GITLAB_URL=https://gitlab.example.com
Environment=METRICS_FILE=/var/log/registry-metrics.log
ExecStart=/opt/registry-monitor.sh
ExecStartPost=/opt/registry-monitor.sh --summary
StandardOutput=journal
StandardError=journal

Create the corresponding timer:

[Unit]
Description=Run GitLab Registry Monitoring Daily
Requires=gitlab-registry-monitor.service

[Timer]
OnCalendar=daily
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target

Create the service user and enable the timer:

sudo useradd --system --shell /bin/false --home-dir /var/lib/gitlab-registry --create-home gitlab-registry
sudo mkdir -p /var/log /var/lib/gitlab-registry
sudo chown gitlab-registry:gitlab-registry /var/log/registry-metrics.log /var/lib/gitlab-registry
sudo chmod 755 /opt/registry-monitor.sh /opt/gitlab-registry-cleanup.sh

Enable and start the timer

sudo systemctl enable --now gitlab-registry-monitor.timer sudo systemctl status gitlab-registry-monitor.timer

Configure cleanup policy alerts

Set up alerting to notify you when cleanup policies fail or when storage usage exceeds thresholds.

Create an alerting script that checks cleanup policy effectiveness:

#!/bin/bash

set -euo pipefail

GITLAB_TOKEN="${GITLAB_TOKEN:-}"
GITLAB_URL="${GITLAB_URL:-https://gitlab.example.com}"
ALERT_EMAIL="${ALERT_EMAIL:-admin@example.com}"
STORAGE_THRESHOLD_GB="${STORAGE_THRESHOLD_GB:-50}"
METRICS_FILE="/var/log/registry-metrics.log"

Function to send alert email

send_alert() { local subject="$1" local message="$2" echo "$message" | mail -s "GitLab Registry Alert: $subject" "$ALERT_EMAIL" echo "Alert sent: $subject" }

Function to check storage thresholds

check_storage_thresholds() { if [[ ! -f "$METRICS_FILE" ]]; then send_alert "Metrics Missing" "Registry metrics file not found at $METRICS_FILE" return 1 fi # Get latest metrics (last 24 hours) yesterday=$(date -d '1 day ago' -u +"%Y-%m-%d") while IFS=',' read -r timestamp project_id project_name repo_count size_bytes; do # Skip header line [[ "$timestamp" == "timestamp" ]] && continue # Check if entry is from last 24 hours entry_date=$(echo "$timestamp" | cut -d'T' -f1) [[ "$entry_date" < "$yesterday" ]] && continue size_gb=$(echo "scale=2; $size_bytes / 1024 / 1024 / 1024" | bc) if (( $(echo "$size_gb > $STORAGE_THRESHOLD_GB" | bc -l) )); then alert_msg="Project '$project_name' (ID: $project_id) is using ${size_gb}GB of registry storage, which exceeds the threshold of ${STORAGE_THRESHOLD_GB}GB. Current statistics:
  • Repositories: $repo_count
  • Total size: ${size_gb}GB
Consider reviewing cleanup policies or manually removing unused images." send_alert "High Storage Usage: $project_name" "$alert_msg" fi done < "$METRICS_FILE" }

Function to check cleanup policy status

check_cleanup_policies() { local config_file="/opt/cleanup-config.json" if [[ ! -f "$config_file" ]]; then send_alert "Configuration Missing" "Cleanup configuration file not found at $config_file" return 1 fi jq -r '.projects[].id' "$config_file" | while read -r project_id; do # Get current policy status policy_response=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "$GITLAB_URL/api/v4/projects/$project_id" || echo "") if [[ -z "$policy_response" ]]; then send_alert "API Error" "Failed to retrieve policy status for project $project_id" continue fi # Check if cleanup policy is enabled policy_enabled=$(echo "$policy_response" | jq -r '.container_expiration_policy.enabled // false') if [[ "$policy_enabled" != "true" ]]; then project_name=$(echo "$policy_response" | jq -r '.name') send_alert "Policy Disabled" "Cleanup policy is disabled for project '$project_name' (ID: $project_id)" fi done }

Main execution

echo "Running registry cleanup alerts..." check_storage_thresholds check_cleanup_policies echo "Cleanup alerts completed"

Add the alerting script to your monitoring timer or create a separate one:

[Unit]
Description=GitLab Container Registry Alerts
After=network.target

[Service]
Type=oneshot
User=gitlab-registry
Group=gitlab-registry
Environment=GITLAB_TOKEN=your_token_here
Environment=GITLAB_URL=https://gitlab.example.com
Environment=ALERT_EMAIL=admin@example.com
Environment=STORAGE_THRESHOLD_GB=50
ExecStart=/opt/cleanup-alerts.sh
StandardOutput=journal
StandardError=journal

Install mail utilities and enable the alert service:

sudo apt install -y mailutils bc
sudo systemctl enable --now gitlab-registry-alerts.service
sudo dnf install -y mailx bc
sudo systemctl enable --now gitlab-registry-alerts.service

Verify your setup

Test your cleanup policy configuration and monitoring setup to ensure everything works correctly.

# Check cleanup policy status via API
curl --header "PRIVATE-TOKEN: your_token" \
     "https://gitlab.example.com/api/v4/projects/123" | \
jq '.container_expiration_policy'

Test monitoring script

/opt/registry-monitor.sh --summary

Check systemd timer status

sudo systemctl status gitlab-registry-monitor.timer sudo systemctl list-timers gitlab-registry-monitor.timer

View recent monitoring logs

sudo journalctl -u gitlab-registry-monitor.service --since="1 day ago"

Test alert script

GITLAB_TOKEN="your_token" STORAGE_THRESHOLD_GB=1 /opt/cleanup-alerts.sh

Check registry storage usage

tail -10 /var/log/registry-metrics.log

You can also verify the cleanup is working by checking your GitLab project's container registry through the web interface. Navigate to your project's Packages and registries > Container Registry to see current images and confirm old images are being removed according to your policies.

Monitor storage usage and cleanup effectiveness

Regular monitoring helps you understand storage patterns and optimize cleanup policies. Set up dashboards and reports to track cleanup effectiveness over time.

#!/bin/bash

METRICS_FILE="/var/log/registry-metrics.log"
REPORT_FILE="/var/log/cleanup-report-$(date +%Y%m%d).txt"

echo "GitLab Container Registry Cleanup Report" > "$REPORT_FILE"
echo "Generated: $(date)" >> "$REPORT_FILE"
echo "==========================================" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

Storage trends over last 30 days

echo "Storage Trends (Last 30 Days):" >> "$REPORT_FILE" echo "--------------------------------" >> "$REPORT_FILE" awk -F',' 'NR>1 {projects[$3] += $5; dates[$1]++} END { print "Total storage by project:" for (p in projects) printf " %s: %.2f GB\n", p, projects[p]/1024/1024/1024 print "\nDays with metrics:", length(dates) }' "$METRICS_FILE" >> "$REPORT_FILE"

Recent cleanup activity

echo "" >> "$REPORT_FILE" echo "Recent Activity (Last 7 Days):" >> "$REPORT_FILE" echo "-------------------------------" >> "$REPORT_FILE" tail -50 "$METRICS_FILE" | awk -F',' ' BEGIN {print "Date,Project,Repositories,Size_GB"} NR>1 {printf "%s,%s,%s,%.2f\n", $1, $3, $4, $5/1024/1024/1024}' >> "$REPORT_FILE" echo "Report saved to: $REPORT_FILE"

The GitLab container registry with SSL certificates and security hardening tutorial provides additional security configuration for your registry setup. For comprehensive infrastructure monitoring, consider setting up Prometheus and Grafana monitoring stack to visualize registry metrics alongside other system metrics.

Common issues

SymptomCauseFix
Cleanup policy not runningPolicy disabled or invalid regexCheck policy settings in GitLab UI, verify regex patterns work with grep
Images not being deletedPreserved by regex patternReview name_regex_keep pattern, test with sample tag names
API calls failingInvalid token or insufficient permissionsVerify token has api scope and user has maintainer role on projects
Too many images deletedOverly broad cleanup rulesUse more specific regex patterns, increase keep_n value
Storage not reducingLarge base layers shared between imagesAllow more time for garbage collection, check for registry maintenance windows
Monitoring script errorsMissing dependencies or permissionsInstall jq, curl, verify script has executable permissions

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it monitored, optimized, and integrated with your deployment pipelines across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.