Implement backup rotation policies with automated cleanup using systemd timers and shell scripts

Intermediate 45 min May 20, 2026 43 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up automated backup rotation with systemd timers to manage storage space, implement retention policies for different backup types, and create monitoring alerts for backup health and cleanup processes.

Prerequisites

  • Root or sudo access
  • Basic understanding of systemd
  • Existing backup system generating files

What this solves

Backup systems generate data continuously, but storage space is finite. Without rotation policies, backup directories fill up, causing new backups to fail and potentially bringing down services. This tutorial implements automated backup cleanup with systemd timers, configurable retention rules, and monitoring to ensure your backup strategy scales without manual intervention.

Step-by-step configuration

Create the backup rotation directory structure

Set up dedicated directories for backup scripts, logs, and configuration files with proper permissions.

sudo mkdir -p /opt/backup-rotation/{scripts,config,logs}
sudo mkdir -p /var/log/backup-rotation
sudo chown -R root:root /opt/backup-rotation
sudo chmod 755 /opt/backup-rotation /opt/backup-rotation/scripts
sudo chmod 644 /opt/backup-rotation/config

Create the main rotation configuration file

Define retention policies for different backup types in a central configuration file that the scripts will read.

# Backup retention configuration

Format: TYPE:PATH:DAILY:WEEKLY:MONTHLY

DAILY = days to keep daily backups

WEEKLY = weeks to keep weekly backups

MONTHLY = months to keep monthly backups

database:/var/backups/database:7:4:3 files:/var/backups/files:14:8:6 logs:/var/backups/logs:30:12:12 config:/var/backups/config:7:8:12

Create the backup rotation script

This script reads the retention configuration and removes old backups based on file age and naming patterns.

#!/bin/bash

set -euo pipefail

Configuration

CONFIG_FILE="/opt/backup-rotation/config/retention.conf" LOG_FILE="/var/log/backup-rotation/rotation.log" LOCK_FILE="/var/run/backup-rotation.lock"

Logging function

log_message() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" }

Check if script is already running

if [ -f "$LOCK_FILE" ]; then log_message "ERROR: Backup rotation already running (lock file exists)" exit 1 fi

Create lock file

echo $$ > "$LOCK_FILE" trap 'rm -f "$LOCK_FILE"' EXIT log_message "Starting backup rotation process"

Read configuration and process each backup type

while IFS=':' read -r backup_type backup_path daily_keep weekly_keep monthly_keep; do # Skip comments and empty lines [[ "$backup_type" =~ ^#.*$ ]] && continue [[ -z "$backup_type" ]] && continue log_message "Processing $backup_type backups in $backup_path" if [ ! -d "$backup_path" ]; then log_message "WARNING: Backup directory $backup_path does not exist" continue fi # Remove daily backups older than specified days find "$backup_path" -name "daily" -type f -mtime +"$daily_keep" -exec rm -f {} \; -print | while read deleted_file; do log_message "Deleted daily backup: $deleted_file" done # Remove weekly backups older than specified weeks weekly_days=$((weekly_keep * 7)) find "$backup_path" -name "weekly" -type f -mtime +"$weekly_days" -exec rm -f {} \; -print | while read deleted_file; do log_message "Deleted weekly backup: $deleted_file" done # Remove monthly backups older than specified months (approximate) monthly_days=$((monthly_keep * 30)) find "$backup_path" -name "monthly" -type f -mtime +"$monthly_days" -exec rm -f {} \; -print | while read deleted_file; do log_message "Deleted monthly backup: $deleted_file" done # Calculate and log space usage if command -v du >/dev/null; then space_used=$(du -sh "$backup_path" | cut -f1) log_message "Current space usage for $backup_type: $space_used" fi done < "$CONFIG_FILE" log_message "Backup rotation completed successfully"

Clean up old rotation logs (keep 30 days)

find /var/log/backup-rotation -name "*.log" -mtime +30 -delete

Make the rotation script executable

Set proper permissions on the backup rotation script so systemd can execute it.

sudo chmod 755 /opt/backup-rotation/scripts/rotate-backups.sh
sudo chown root:root /opt/backup-rotation/scripts/rotate-backups.sh

Create the systemd service unit

Define a systemd service that runs the backup rotation script with proper logging and error handling.

[Unit]
Description=Backup Rotation Service
After=network.target

[Service]
Type=oneshot
User=root
Group=root
ExecStart=/opt/backup-rotation/scripts/rotate-backups.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=backup-rotation

Resource limits

MemoryMax=512M CPUQuota=25%

Security hardening

PrivateTmp=true ProtectSystem=strict ReadWritePaths=/var/backups /var/log/backup-rotation /var/run NoNewPrivileges=true ProtectHome=true ProtectControlGroups=true ProtectKernelModules=true ProtectKernelTunables=true

Create the systemd timer unit

Set up a timer that runs backup rotation daily at 2 AM with randomization to prevent resource conflicts.

[Unit]
Description=Daily Backup Rotation Timer
Requires=backup-rotation.service

[Timer]
OnCalendar=--* 02:00:00
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target

Enable and start the systemd timer

Reload systemd configuration and enable the timer to start automatically on boot.

sudo systemctl daemon-reload
sudo systemctl enable backup-rotation.timer
sudo systemctl start backup-rotation.timer

Create a backup health monitoring script

This script checks backup status, disk usage, and rotation process health for monitoring integration.

#!/bin/bash

set -euo pipefail

Configuration

CONFIG_FILE="/opt/backup-rotation/config/retention.conf" LOG_FILE="/var/log/backup-rotation/health-check.log" ALERT_THRESHOLD_PERCENT=85 MAX_AGE_HOURS=26

Exit codes for monitoring systems

OK=0 WARNING=1 CRITICAL=2 log_message() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } exit_code=$OK alert_messages=() log_message "Starting backup health check"

Check each backup location

while IFS=':' read -r backup_type backup_path daily_keep weekly_keep monthly_keep; do [[ "$backup_type" =~ ^#.*$ ]] && continue [[ -z "$backup_type" ]] && continue if [ ! -d "$backup_path" ]; then alert_messages+=("CRITICAL: Backup directory $backup_path missing") exit_code=$CRITICAL continue fi # Check disk usage usage_percent=$(df "$backup_path" | tail -1 | awk '{print int($5)}') if [ "$usage_percent" -gt "$ALERT_THRESHOLD_PERCENT" ]; then alert_messages+=("WARNING: $backup_type backup disk usage at ${usage_percent}%") [ $exit_code -eq $OK ] && exit_code=$WARNING fi # Check for recent backups recent_backup=$(find "$backup_path" -type f -mtime -1 | head -1) if [ -z "$recent_backup" ]; then alert_messages+=("WARNING: No recent backups found in $backup_path") [ $exit_code -eq $OK ] && exit_code=$WARNING fi # Check backup count backup_count=$(find "$backup_path" -type f | wc -l) log_message "$backup_type: $backup_count files, ${usage_percent}% disk usage" done < "$CONFIG_FILE"

Check rotation service status

if ! systemctl is-active --quiet backup-rotation.timer; then alert_messages+=("CRITICAL: Backup rotation timer not running") exit_code=$CRITICAL fi

Check for recent rotation log entries

last_rotation=$(find /var/log/backup-rotation -name "rotation.log" -mtime -1 2>/dev/null | head -1) if [ -z "$last_rotation" ]; then alert_messages+=("WARNING: No recent rotation logs found") [ $exit_code -eq $OK ] && exit_code=$WARNING fi

Output results for monitoring systems

if [ ${#alert_messages[@]} -eq 0 ]; then echo "OK: All backup systems healthy" log_message "Health check passed" else for message in "${alert_messages[@]}"; do echo "$message" log_message "$message" done fi exit $exit_code

Make the health check script executable

Set permissions on the monitoring script and create a systemd timer to run it regularly.

sudo chmod 755 /opt/backup-rotation/scripts/backup-health-check.sh
sudo chown root:root /opt/backup-rotation/scripts/backup-health-check.sh

Create health check systemd service and timer

Set up automated health checking that runs every hour to catch issues early.

[Unit]
Description=Backup Health Check
After=network.target

[Service]
Type=oneshot
User=root
Group=root
ExecStart=/opt/backup-rotation/scripts/backup-health-check.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=backup-health
[Unit]
Description=Hourly Backup Health Check
Requires=backup-health-check.service

[Timer]
OnCalendar=hourly
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target

Enable the health check timer

Start the health monitoring system alongside the backup rotation.

sudo systemctl daemon-reload
sudo systemctl enable backup-health-check.timer
sudo systemctl start backup-health-check.timer

Create example backup directories for testing

Set up test backup directories that match your retention configuration to verify the system works.

sudo mkdir -p /var/backups/{database,files,logs,config}
sudo chown root:root /var/backups/{database,files,logs,config}
sudo chmod 755 /var/backups/{database,files,logs,config}

Create test backup files

Generate sample backup files with different ages to test the rotation logic.

# Create test files with different naming patterns and ages
sudo touch /var/backups/database/db-backup-daily-$(date +%Y%m%d).sql
sudo touch /var/backups/database/db-backup-weekly-$(date +%Y%W).sql
sudo touch /var/backups/files/files-backup-daily-$(date +%Y%m%d).tar.gz

Create an old file to test deletion (10 days old)

sudo touch -d "10 days ago" /var/backups/database/old-daily-backup.sql sudo touch -d "60 days ago" /var/backups/database/old-weekly-backup.sql

Verify your setup

Check that the systemd timers are running and test the rotation manually.

# Check timer status
sudo systemctl status backup-rotation.timer
sudo systemctl status backup-health-check.timer

List all active timers

sudo systemctl list-timers backup-*

Test the rotation script manually

sudo /opt/backup-rotation/scripts/rotate-backups.sh

Check rotation logs

sudo tail -f /var/log/backup-rotation/rotation.log

Test health check

sudo /opt/backup-rotation/scripts/backup-health-check.sh echo "Exit code: $?"

Verify that old test files were removed and check the log output for any errors.

# Check if old files were cleaned up
ls -la /var/backups/database/

View systemd journal for the services

journalctl -u backup-rotation.service -f journalctl -u backup-health-check.service -f

Advanced retention policies

Create size-based rotation script

Add a script that removes backups when directories exceed size limits, useful for high-volume backup systems.

#!/bin/bash

set -euo pipefail

Configuration - sizes in GB

MAX_SIZE_GB=50 CONFIG_FILE="/opt/backup-rotation/config/retention.conf" LOG_FILE="/var/log/backup-rotation/size-cleanup.log" log_message() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } log_message "Starting size-based cleanup" while IFS=':' read -r backup_type backup_path daily_keep weekly_keep monthly_keep; do [[ "$backup_type" =~ ^#.*$ ]] && continue [[ -z "$backup_type" ]] && continue if [ ! -d "$backup_path" ]; then continue fi # Get current size in GB current_size_kb=$(du -s "$backup_path" | cut -f1) current_size_gb=$((current_size_kb / 1024 / 1024)) log_message "$backup_type current size: ${current_size_gb}GB (limit: ${MAX_SIZE_GB}GB)" if [ "$current_size_gb" -gt "$MAX_SIZE_GB" ]; then log_message "Size limit exceeded for $backup_type, removing oldest files" # Remove oldest files until under limit while [ "$current_size_gb" -gt "$MAX_SIZE_GB" ]; do oldest_file=$(find "$backup_path" -type f -printf '%T@ %p\n' | sort -n | head -1 | cut -d' ' -f2-) if [ -z "$oldest_file" ]; then log_message "No more files to remove in $backup_path" break fi rm -f "$oldest_file" log_message "Removed oldest file: $oldest_file" # Recalculate size current_size_kb=$(du -s "$backup_path" | cut -f1) current_size_gb=$((current_size_kb / 1024 / 1024)) done fi done < "$CONFIG_FILE" log_message "Size-based cleanup completed"

Set up email alerts for backup failures

Configure email notifications when backup rotation or health checks fail. This requires a configured mail system.

#!/bin/bash

set -euo pipefail

Configuration

ALERT_EMAIL="admin@example.com" SMTP_SERVER="localhost" HOSTNAME=$(hostname -f)

Function to send email alert

send_alert() { local subject="$1" local message="$2" if command -v mail >/dev/null; then echo -e "$message\n\nHost: $HOSTNAME\nTime: $(date)" | mail -s "$subject" "$ALERT_EMAIL" elif command -v sendmail >/dev/null; then { echo "To: $ALERT_EMAIL" echo "Subject: $subject" echo "" echo -e "$message\n\nHost: $HOSTNAME\nTime: $(date)" } | sendmail "$ALERT_EMAIL" else echo "No mail command available" >&2 return 1 fi }

Check if health check failed

if ! /opt/backup-rotation/scripts/backup-health-check.sh >/dev/null 2>&1; then health_output=$(/opt/backup-rotation/scripts/backup-health-check.sh 2>&1) send_alert "Backup Health Check Failed - $HOSTNAME" "$health_output" fi

Integrate with monitoring systems

Create a script that outputs metrics in Prometheus format for integration with monitoring stacks.

#!/bin/bash

set -euo pipefail

CONFIG_FILE="/opt/backup-rotation/config/retention.conf"
METRICS_FILE="/var/lib/node_exporter/textfile_collector/backup_metrics.prom"

Ensure directory exists

sudo mkdir -p /var/lib/node_exporter/textfile_collector

Generate Prometheus metrics

{ echo "# HELP backup_files_total Total number of backup files" echo "# TYPE backup_files_total gauge" echo "# HELP backup_disk_usage_bytes Disk usage of backup directories" echo "# TYPE backup_disk_usage_bytes gauge" echo "# HELP backup_rotation_last_run_timestamp Last successful rotation run" echo "# TYPE backup_rotation_last_run_timestamp gauge" while IFS=':' read -r backup_type backup_path daily_keep weekly_keep monthly_keep; do [[ "$backup_type" =~ ^#.*$ ]] && continue [[ -z "$backup_type" ]] && continue if [ -d "$backup_path" ]; then file_count=$(find "$backup_path" -type f | wc -l) disk_usage=$(du -sb "$backup_path" | cut -f1) echo "backup_files_total{type=\"$backup_type\",path=\"$backup_path\"} $file_count" echo "backup_disk_usage_bytes{type=\"$backup_type\",path=\"$backup_path\"} $disk_usage" fi done < "$CONFIG_FILE" # Add timestamp of last rotation if [ -f "/var/log/backup-rotation/rotation.log" ]; then last_run=$(stat -c %Y /var/log/backup-rotation/rotation.log) echo "backup_rotation_last_run_timestamp $last_run" fi } > "$METRICS_FILE.tmp" && mv "$METRICS_FILE.tmp" "$METRICS_FILE"

Storage optimization strategies

Note: These strategies help maximize backup retention while minimizing storage costs and maintaining recovery capabilities.

Implement compression for old backups

Automatically compress backups older than a certain age to save storage space without losing data.

#!/bin/bash

set -euo pipefail

CONFIG_FILE="/opt/backup-rotation/config/retention.conf"
LOG_FILE="/var/log/backup-rotation/compression.log"
COMPRESS_AFTER_DAYS=7

log_message() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log_message "Starting backup compression for files older than $COMPRESS_AFTER_DAYS days"

while IFS=':' read -r backup_type backup_path daily_keep weekly_keep monthly_keep; do
    [[ "$backup_type" =~ ^#.*$ ]] && continue
    [[ -z "$backup_type" ]] && continue
    
    if [ ! -d "$backup_path" ]; then
        continue
    fi
    
    log_message "Processing compression for $backup_type in $backup_path"
    
    # Find uncompressed files older than specified days
    find "$backup_path" -type f ! -name ".gz" ! -name ".xz" -mtime +"$COMPRESS_AFTER_DAYS" | while read -r file; do
        if [ -f "$file" ]; then
            log_message "Compressing: $file"
            
            # Use xz for better compression ratio
            if xz -9 "$file"; then
                log_message "Successfully compressed: $file"
            else
                log_message "ERROR: Failed to compress: $file"
            fi
        fi
    done
    
done < "$CONFIG_FILE"

log_message "Backup compression completed"

Add the compression script to systemd

Create a weekly timer to automatically compress old backups and save storage space.

[Unit]
Description=Backup Compression Service
After=network.target

[Service]
Type=oneshot
User=root
Group=root
ExecStart=/opt/backup-rotation/scripts/compress-old-backups.sh
StandardOutput=journal
StandardError=journal
[Unit]
Description=Weekly Backup Compression
Requires=backup-compression.service

[Timer]
OnCalendar=Sun 03:00:00
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target

Enable compression automation

Make the compression scripts executable and enable the systemd timer.

sudo chmod 755 /opt/backup-rotation/scripts/compress-old-backups.sh
sudo chmod 755 /opt/backup-rotation/scripts/backup-metrics.sh
sudo systemctl daemon-reload
sudo systemctl enable backup-compression.timer
sudo systemctl start backup-compression.timer

Common issues

Symptom Cause Fix
Rotation script fails with permission denied Incorrect ownership or permissions on backup directories sudo chown -R root:root /var/backups && sudo chmod -R 755 /var/backups
Timer not running after reboot Timer not properly enabled sudo systemctl enable backup-rotation.timer && sudo systemctl start backup-rotation.timer
Old files not being deleted File naming doesn't match patterns in script Update retention.conf paths or modify find patterns in rotation script
High CPU usage during rotation Large number of files being processed Add nice -n 10 to ExecStart in service file and reduce CPUQuota
Health check always reports warnings Threshold values too strict for your environment Adjust ALERT_THRESHOLD_PERCENT and MAX_AGE_HOURS in health check script
Lock file prevents rotation from running Previous rotation process crashed or was killed sudo rm -f /var/run/backup-rotation.lock then restart timer

Monitor backup rotation with logging and alerts

The backup rotation system generates comprehensive logs that integrate with existing monitoring infrastructure. You can connect these logs to centralized logging systems like the ELK stack or set up Prometheus monitoring for automated alerting.

Set up log aggregation with rsyslog

Forward backup rotation logs to a central logging server for analysis and long-term retention.

# Backup rotation log forwarding
:programname, isequal, "backup-rotation" @@logserver.example.com:514
:programname, isequal, "backup-health" @@logserver.example.com:514

Local file logging with rotation

:programname, isequal, "backup-rotation" /var/log/backup-rotation/systemd.log :programname, isequal, "backup-health" /var/log/backup-rotation/health-systemd.log & stop

Configure logrotate for backup rotation logs

Prevent log files from growing too large by setting up automatic log rotation.

/var/log/backup-rotation/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    copytruncate
    sharedscripts
    postrotate
        systemctl reload rsyslog
    endscript
}

Test log rotation and restart rsyslog

Apply the logging configuration and verify it works correctly.

sudo systemctl restart rsyslog
sudo logrotate -d /etc/logrotate.d/backup-rotation
sudo journalctl -u backup-rotation.service -n 20

Next steps

Running this in production?

Want this handled for you? Setting up backup rotation once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

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