A Go-based daemon for automatically deleting old Helm releases. Runs continuously in your cluster and prunes stale releases at configurable intervals.
- Daemon mode — Runs continuously with configurable prune intervals
- Native Helm SDK — Uses Helm Go SDK directly (no CLI shelling)
- Flexible filtering — Filter by release name, namespace, age, or count
- Regex support — Include/exclude releases and namespaces using regex patterns
- Namespace cleanup — Optionally delete empty namespaces after pruning
- Health endpoints — Built-in
/healthz,/readyz, and/metricsfor Kubernetes probes - Prometheus metrics — Exposes metrics for monitoring prune operations
- Graceful shutdown — Handles SIGTERM/SIGINT for clean pod termination
- Rate limiting — Configurable rate limiting to avoid overwhelming the API server
- Dry-run mode — Preview what would be deleted before making changes
- Run-once mode — Single execution for CI/CD pipelines or CronJobs (
--once) - Minimal image — Alpine-based container with non-root user
docker pull quay.io/fairwinds/helm-release-pruner:latestgo build -o helm-release-pruner ./cmd/prunerhelm-release-pruner [flags]The pruner runs as a daemon by default, executing prune cycles at the configured interval.
| Flag | Default | Description |
|---|---|---|
--interval |
1h |
How often to run the pruning cycle |
--max-releases-to-keep |
0 |
Keep only the N most recent releases globally (0 = no limit) |
--older-than |
Delete releases older than this duration | |
--release-filter |
Regex to include matching release names | |
--namespace-filter |
Regex to include matching namespaces | |
--release-exclude |
Regex to exclude matching release names | |
--namespace-exclude |
Regex to exclude matching namespaces | |
--preserve-namespace |
false |
Don't delete empty namespaces |
--cleanup-orphan-namespaces |
false |
Delete namespaces with no Helm releases (requires --orphan-namespace-filter) |
--orphan-namespace-filter |
Regex filter for orphan namespace cleanup (required with --cleanup-orphan-namespaces) |
|
--orphan-namespace-exclude |
Regex to exclude namespaces from orphan cleanup | |
--system-namespaces |
Comma-separated additional namespaces to never delete | |
--delete-rate-limit |
100ms |
Minimum duration between delete operations (0 to disable) |
--dry-run |
false |
Show what would be deleted |
--once |
false |
Run a single prune cycle and exit (for CronJobs) |
--debug |
false |
Enable debug logging |
--health-addr |
:8080 |
Address for health check and metrics endpoints |
The --older-than and --interval flags support:
- Standard Go durations:
1h,336h,720h30m - Days:
14d(14 days) - Weeks:
2w(2 weeks)
Run daemon that prunes releases older than 2 weeks, checking every hour:
helm-release-pruner --older-than=2w --interval=1hPrune feature branch releases every 30 minutes:
helm-release-pruner \
--interval=30m \
--older-than=1w \
--release-filter="^feature-.+-web$" \
--namespace-filter="^feature-.+"Keep only the 5 most recent releases globally (after filtering), excluding permanent releases:
helm-release-pruner \
--interval=6h \
--max-releases-to-keep=5 \
--release-exclude="-permanent$"Dry run to preview deletions:
helm-release-pruner --older-than=30d --dry-runAdd custom system namespaces that should never be deleted:
helm-release-pruner \
--older-than=2w \
--system-namespaces="monitoring,logging,istio-system"The daemon exposes health and metrics endpoints for Kubernetes probes and monitoring:
| Endpoint | Description |
|---|---|
/healthz |
Liveness probe - returns 200 if process is running |
/readyz |
Readiness probe - returns 200 after initialization and if cluster is reachable |
/metrics |
Prometheus metrics endpoint |
| Metric | Type | Description |
|---|---|---|
helm_pruner_releases_deleted_total |
Counter | Total number of Helm releases deleted |
helm_pruner_namespaces_deleted_total |
Counter | Total number of namespaces deleted |
helm_pruner_cycle_duration_seconds |
Histogram | Duration of prune cycles in seconds |
helm_pruner_cycle_failures_total |
Counter | Total number of failed prune cycles |
helm_pruner_releases_scanned_total |
Counter | Total number of releases scanned across all cycles |
Deploy as a Deployment (not CronJob) since it runs as a daemon:
apiVersion: apps/v1
kind: Deployment
metadata:
name: helm-release-pruner
spec:
replicas: 1
selector:
matchLabels:
app: helm-release-pruner
template:
metadata:
labels:
app: helm-release-pruner
spec:
serviceAccountName: helm-release-pruner
containers:
- name: pruner
image: quay.io/fairwinds/helm-release-pruner:latest
args:
- --interval=1h
- --older-than=2w
ports:
- name: health
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128MiapiVersion: v1
kind: ServiceAccount
metadata:
name: helm-release-pruner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: helm-release-pruner
rules:
# List and delete Helm releases (stored as secrets by default)
# If using HELM_DRIVER=configmap, change "secrets" to "configmaps"
- apiGroups: [""]
resources: ["secrets"]
verbs: ["list", "get", "delete"]
# Optional: delete empty namespaces
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "get", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: helm-release-pruner
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: helm-release-pruner
subjects:
- kind: ServiceAccount
name: helm-release-pruner
namespace: defaultapiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: helm-release-pruner
spec:
selector:
matchLabels:
app: helm-release-pruner
endpoints:
- port: health
path: /metrics
interval: 30sThe Go version maintains CLI compatibility where possible:
| Old Flag (Bash) | New Flag (Go) | Notes |
|---|---|---|
--helm-release-filter |
--release-filter |
Same regex behavior |
--helm-release-negate-filter |
--release-exclude |
Same regex behavior |
--namespace-negate-filter |
--namespace-exclude |
Same regex behavior |
--older-than="4 weeks ago" |
--older-than=4w |
Uses Go duration format |
--preserve-namespace |
--preserve-namespace |
Bug fix: Now works correctly (was broken in bash) |
--max-releases-to-keep |
--max-releases-to-keep |
Same global behavior |
| one-shot / cron | --once |
Run a single cycle and exit (for CronJobs) |
| N/A | --debug |
New flag for verbose logging |
Key changes:
- Daemon mode: Now runs continuously instead of one-shot. Use
--intervalto control how often the pruning cycle runs (default: 1h). - Duration format: Uses Go duration format (
2w,14d,336h) instead of GNU date strings ("2 weeks ago"). - Health endpoints: Exposes
/healthz,/readyz, and/metricson:8080by default. - Prometheus metrics: Exposes metrics for monitoring prune operations.
- Rate limiting: Default 100ms between delete operations to avoid overwhelming the API server.
- System namespace protection: System namespaces (
kube-system,kube-public,default,kube-node-lease) are never deleted, even if they match filters. Add more with--system-namespaces. - Graceful shutdown: Handles SIGTERM/SIGINT properly for clean pod termination.
- Bug fix:
--preserve-namespacenow works correctly. The bash version had a bug where the flag was parsed but the variable was never used.
Behavioral clarification:
The --max-releases-to-keep flag applies globally across all filtered releases, not per release name. For example, with --max-releases-to-keep=5 and --release-filter="^feature-":
- The pruner finds all releases matching
^feature- - Sorts them by deployment time (newest first)
- Keeps the 5 most recently deployed releases
- Deletes all others
This is the same behavior as the original bash script. If you need to keep N releases per application, use separate pruner instances with specific filters.
- Go 1.25+ (see go.mod)
- Access to a Kubernetes cluster (for testing)
# Build binary
go build -o helm-release-pruner ./cmd/pruner
# Build Docker image
docker build -t helm-release-pruner:dev .
# Run tests
go test ./...# Uses your local kubeconfig
./helm-release-pruner --dry-run --older-than=1w --interval=5m --debugThe goal of the Fairwinds Community is to exchange ideas, influence the open source roadmap, and network with fellow Kubernetes users. Chat with us on Slack join the user group to get involved!
Enjoying helm-release-pruner? Check out some of our other projects:
- Polaris - Audit, enforce, and build policies for Kubernetes resources, including over 20 built-in checks for best practices
- Goldilocks - Right-size your Kubernetes Deployments by compare your memory and CPU settings against actual usage
- Pluto - Detect Kubernetes resources that have been deprecated or removed in future versions
- Nova - Check to see if any of your Helm charts have updates available
- rbac-manager - Simplify the management of RBAC in your Kubernetes clusters