Skip to main content

Configure Retention Policies

Set up automated cleanup of old container images using CEL-based retention policies.

Prerequisites

  • Angos running
  • update_pull_time = true if using pull-based retention

How Retention Works

Retention policies define which images to keep. Images not matching any rule are eligible for deletion when running scrub --retention.

Protected manifests are never deleted:

  • Child manifests of multi-platform indexes
  • Manifests with referrers (signatures, SBOMs)

Basic Configuration

Global Policy

[global]
update_pull_time = true # Required for pull-based retention

[global.retention_policy]
rules = [
'image.tag == "latest"',
'image.pushed_at > now() - days(30)'
]

Repository Policy

[repository."production".retention_policy]
rules = [
'image.tag == "latest"',
'image.pushed_at > now() - days(90)',
'top_pushed(20)'
]

Common Patterns

Keep Tagged, Delete Untagged

rules = [
'image.tag != null'
]

Time-Based Retention

rules = [
'image.pushed_at > now() - days(30)', # Keep 30 days
'image.last_pulled_at > now() - days(7)' # Or pulled within 7 days
]

Top-K Retention

rules = [
'top_pushed(10)', # Keep 10 most recently pushed
'top_pulled(5)' # Keep 5 most recently pulled
]

Semantic Version Tags

rules = [
'image.tag != null && image.tag.matches("^v?[0-9]+\\.[0-9]+\\.[0-9]+$")'
]

Combined Rules

A manifest is kept if any rule matches:

rules = [
# Always keep latest
'image.tag == "latest"',

# Keep release tags forever
'image.tag != null && image.tag.matches("^v[0-9]+\\.[0-9]+\\.[0-9]+$")',

# Keep other tags for 30 days
'image.tag != null && image.pushed_at > now() - days(30)',

# Keep untagged for 7 days
'image.pushed_at > now() - days(7)',

# Keep top 10 most pulled
'top_pulled(10)'
]

Environment-Specific Policies

Development: Aggressive Cleanup

[repository."dev".retention_policy]
rules = [
'image.tag == "latest"',
'image.pushed_at > now() - days(7)',
'top_pushed(5)'
]

Staging: Moderate Retention

[repository."staging".retention_policy]
rules = [
'image.tag == "latest"',
'image.pushed_at > now() - days(14)',
'top_pushed(10)'
]

Production: Conservative

[repository."production".retention_policy]
rules = [
'image.tag == "latest"',
'image.pushed_at > now() - days(90)',
'image.tag != null && image.tag.matches("^v[0-9]+\\.")',
'top_pushed(50)'
]

Global + Repository Policies

When both are defined, a manifest is kept if either policy matches:

# Global baseline: keep everything for at least 7 days
[global.retention_policy]
rules = [
'image.pushed_at > now() - days(7)'
]

# Repo-specific: extend for production
[repository."production".retention_policy]
rules = [
'image.pushed_at > now() - days(365)'
]

Result: Production images kept for 365 days, others for 7 days.


Enforcing Retention Policies

Retention policies are enforced by the scrub command with the -r flag:

# Preview what would be deleted
./angos -c config.toml scrub --retention --dry-run

# Enforce retention policies
./angos -c config.toml scrub --retention

Scheduled Enforcement

Cron:

0 3 * * * /usr/bin/angos -c /etc/registry/config.toml scrub --retention

Kubernetes CronJob:

apiVersion: batch/v1
kind: CronJob
metadata:
name: registry-retention
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: scrub
image: ghcr.io/project-angos/angos:latest
args: ["-c", "/config/config.toml", "scrub", "--retention"]
restartPolicy: OnFailure

Verification

Check What Would Be Deleted

RUST_LOG=info ./angos scrub --retention --dry-run

List Current Manifests

curl http://localhost:5000/v2/_ext/myrepo/myimage/_revisions | jq

Troubleshooting

Images not being deleted:

  • Check if they match any retention rule
  • Check if they're protected (index child or has referrers)
  • Verify scrub command is running

Pull time not tracked:

  • Enable update_pull_time = true in global config
  • Pull times are only tracked after enabling

Rules not matching:

  • Use debug logging: RUST_LOG=angos::command::scrub=debug
  • Check that image.tag is null for untagged manifests

Reference