Files
kennethreitz f06d489bbc scripts: add prune-backups.sh + daily cron for backup retention
Keeps newest KEEP (default 7) MinIO backup object dirs per job, pruning
the rest by full path (object dirs are multipart, not real .zip files).
Deployed to /usr/local/bin and run daily 04:00 UTC via
/etc/cron.d/prune-backups -> /var/log/prune-backups.log. Closes the
+3G/day unbounded-backup growth flagged in the 2026-06-28 cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:11:25 -04:00

54 lines
2.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Prune old Dokploy/MinIO backup objects to a retention count.
#
# Dokploy stores each scheduled backup as a MinIO multipart OBJECT, which on the
# single-node fs backend is a *directory* of `part.NN` chunks + `xl.meta` named
# like `webserver-backup-2026-06-28T00-00-00-097Z.zip`. Their ISO-8601 names sort
# chronologically, so "keep newest N" = keep the last N after a lexical sort.
#
# Runs ON the server (mercury) — see the cron block in docs/server.md.
#
# Usage:
# scripts/prune-backups.sh # keep 7 (default), really delete
# KEEP=14 scripts/prune-backups.sh # keep 14
# DRY_RUN=1 scripts/prune-backups.sh # show what would be deleted, delete nothing
#
# Env:
# KEEP number of most-recent backups to keep per job dir (default 7)
# BASE backups root (default /var/lib/minio-infra/dokploy-backups)
# DRY_RUN set to 1 to only print
set -euo pipefail
KEEP="${KEEP:-7}"
BASE="${BASE:-/var/lib/minio-infra/dokploy-backups}"
DRY_RUN="${DRY_RUN:-0}"
[[ "$KEEP" =~ ^[0-9]+$ && "$KEEP" -ge 1 ]] || { echo "KEEP must be a positive integer" >&2; exit 2; }
[[ -d "$BASE" ]] || { echo "backups root not found: $BASE" >&2; exit 1; }
ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
deleted_total=0
# Each immediate subdir of BASE is one backup JOB; its child dirs are the backup objects.
for job in "$BASE"/*/; do
[[ -d "$job" ]] || continue
jobname="$(basename "$job")"
# Object dirs, oldest first; drop the newest KEEP, prune the rest by full path.
mapfile -t victims < <(find "$job" -mindepth 1 -maxdepth 1 -type d | sort | head -n "-$KEEP")
(( ${#victims[@]} )) || { echo "$(ts) [$jobname] nothing to prune (<= $KEEP backups)"; continue; }
for obj in "${victims[@]}"; do
if [[ "$DRY_RUN" == "1" ]]; then
echo "$(ts) [$jobname] WOULD delete $(basename "$obj") ($(du -sh "$obj" 2>/dev/null | cut -f1))"
else
sz="$(du -sh "$obj" 2>/dev/null | cut -f1)"
rm -rf "$obj"
echo "$(ts) [$jobname] deleted $(basename "$obj") ($sz)"
((deleted_total++))
fi
done
done
echo "$(ts) done — keep=$KEEP, deleted=$deleted_total, dry_run=$DRY_RUN, df=$(df -h "$BASE" | awk 'NR==2{print $5" used, "$4" free"}')"