Set up automated Linux system backups using rsync with SSH authentication, systemd timers for scheduling, retention policies, email notifications, and monitoring. Perfect for production environments requiring reliable backup automation.
Prerequisites
- Root or sudo access
- At least 10GB free disk space for local backups
- Network connectivity for remote backups
- Remote server with SSH access (optional)
What this solves
System backups are critical for data protection, but manual backups are unreliable and time-consuming. This tutorial shows you how to configure automated Linux system backups using rsync for efficient file synchronization, systemd timers for precise scheduling, and proper retention policies to manage storage. You'll also set up email notifications for backup success and failure monitoring.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest versions of all required tools.
sudo apt update && sudo apt upgrade -y
Install required packages
Install rsync, SSH client, and mail utilities for backup operations and notifications.
sudo apt install -y rsync openssh-client mailutils postfix
Create backup user and directories
Create a dedicated backup user with proper permissions for security isolation.
sudo useradd --system --shell /bin/bash --home /opt/backup --create-home backup
sudo mkdir -p /opt/backup/{scripts,logs,local-backup}
sudo chown -R backup:backup /opt/backup
sudo chmod 755 /opt/backup
sudo chmod 750 /opt/backup/{scripts,logs,local-backup}
Generate SSH key for remote backups
Create an SSH key pair for passwordless authentication to remote backup servers.
sudo -u backup ssh-keygen -t ed25519 -f /opt/backup/.ssh/backup_key -N "" -C "backup@$(hostname)"
sudo -u backup chmod 700 /opt/backup/.ssh
sudo -u backup chmod 600 /opt/backup/.ssh/backup_key
sudo -u backup chmod 644 /opt/backup/.ssh/backup_key.pub
Configure SSH client settings
Create SSH configuration for consistent connection settings and security.
Host backup-server
HostName 203.0.113.10
User backup
Port 22
IdentityFile /opt/backup/.ssh/backup_key
StrictHostKeyChecking no
UserKnownHostsFile /opt/backup/.ssh/known_hosts
ServerAliveInterval 60
ServerAliveCountMax 3
Compression yes
sudo chown backup:backup /opt/backup/.ssh/config
sudo chmod 600 /opt/backup/.ssh/config
Create backup configuration file
Define backup sources, destinations, and retention settings in a central configuration file.
# Backup Configuration
BACKUP_NAME="$(hostname)-system"
LOCAL_BACKUP_DIR="/opt/backup/local-backup"
REMOTE_BACKUP_HOST="backup-server"
REMOTE_BACKUP_DIR="/backup/servers/$(hostname)"
LOG_FILE="/opt/backup/logs/backup.log"
RETENTION_DAYS=30
EMAIL_RECIPIENT="admin@example.com"
Source directories to backup (space-separated)
BACKUP_SOURCES="/etc /var/log /var/www /home /opt/important-data"
Exclude patterns (space-separated)
EXCLUDE_PATTERNS="/var/log/.log. /var/log/journal/ /tmp/ /var/tmp/ /proc/ /sys/ /dev/ /run/*"
Rsync options
RSYNC_OPTS="-avz --delete --delete-excluded --numeric-ids --hard-links --acls --xattrs"
sudo chown backup:backup /opt/backup/scripts/backup.conf
sudo chmod 640 /opt/backup/scripts/backup.conf
Create main backup script
Build the main backup script with logging, error handling, and retention management.
#!/bin/bash
Load configuration
source /opt/backup/scripts/backup.conf
Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S'): $1" | tee -a "$LOG_FILE"
}
Function to send email notification
send_notification() {
local subject="$1"
local message="$2"
echo "$message" | mail -s "[$(hostname)] $subject" "$EMAIL_RECIPIENT"
}
Function to cleanup old backups
cleanup_old_backups() {
log_message "Cleaning up backups older than $RETENTION_DAYS days"
find "$LOCAL_BACKUP_DIR" -type d -name "$BACKUP_NAME-*" -mtime +$RETENTION_DAYS -exec rm -rf {} + 2>/dev/null
# Remote cleanup if SSH connection works
if ssh "$REMOTE_BACKUP_HOST" "test -d $REMOTE_BACKUP_DIR" 2>/dev/null; then
ssh "$REMOTE_BACKUP_HOST" "find $REMOTE_BACKUP_DIR -type d -name '$BACKUP_NAME-*' -mtime +$RETENTION_DAYS -exec rm -rf {} +" 2>/dev/null
fi
}
Function to perform backup
perform_backup() {
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_dest="$LOCAL_BACKUP_DIR/$BACKUP_NAME-$timestamp"
local rsync_success=true
log_message "Starting backup to $backup_dest"
# Create backup directory
mkdir -p "$backup_dest"
# Build exclude options for rsync
local exclude_opts=""
for pattern in $EXCLUDE_PATTERNS; do
exclude_opts="$exclude_opts --exclude='$pattern'"
done
# Perform local backup
for source in $BACKUP_SOURCES; do
if [ -d "$source" ] || [ -f "$source" ]; then
log_message "Backing up $source"
eval "rsync $RSYNC_OPTS $exclude_opts '$source' '$backup_dest/'"
if [ $? -ne 0 ]; then
log_message "ERROR: Failed to backup $source"
rsync_success=false
fi
else
log_message "WARNING: Source $source does not exist, skipping"
fi
done
# Create backup metadata
cat > "$backup_dest/backup_info.txt" << EOF
Backup Date: $(date)
Hostname: $(hostname)
Backup Sources: $BACKUP_SOURCES
Backup Size: $(du -sh "$backup_dest" | cut -f1)
Rsync Options: $RSYNC_OPTS
EOF
if [ "$rsync_success" = true ]; then
log_message "Local backup completed successfully"
# Sync to remote server
if ssh "$REMOTE_BACKUP_HOST" "mkdir -p $REMOTE_BACKUP_DIR" 2>/dev/null; then
log_message "Syncing to remote server"
rsync $RSYNC_OPTS "$backup_dest/" "$REMOTE_BACKUP_HOST:$REMOTE_BACKUP_DIR/$BACKUP_NAME-$timestamp/"
if [ $? -eq 0 ]; then
log_message "Remote backup completed successfully"
send_notification "Backup Success" "Backup completed successfully at $timestamp\nLocal: $backup_dest\nRemote: $REMOTE_BACKUP_HOST:$REMOTE_BACKUP_DIR/$BACKUP_NAME-$timestamp"
else
log_message "ERROR: Remote backup failed"
send_notification "Backup Warning" "Local backup succeeded but remote sync failed at $timestamp\nLocal: $backup_dest"
fi
else
log_message "WARNING: Cannot connect to remote server, keeping local backup only"
send_notification "Backup Warning" "Local backup succeeded but remote server unavailable at $timestamp\nLocal: $backup_dest"
fi
else
log_message "ERROR: Local backup failed"
send_notification "Backup Failed" "Backup failed at $timestamp\nCheck log: $LOG_FILE"
return 1
fi
}
Main execution
log_message "=== Backup process started ==="nperform_backup
cleanup_old_backups
log_message "=== Backup process completed ==="
sudo chown backup:backup /opt/backup/scripts/backup.sh
sudo chmod 750 /opt/backup/scripts/backup.sh
Create systemd service unit
Define the systemd service that will execute the backup script with proper user context and security settings.
[Unit]
Description=System Backup Service
Wants=network-online.target
After=network-online.target
Requires=local-fs.target
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/opt/backup/scripts/backup.sh
WorkingDirectory=/opt/backup
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
StandardOutput=journal
StandardError=journal
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/backup
NoNewPrivileges=true
RestrictSUIDSGID=true
Create systemd timer unit
Configure the systemd timer to run backups automatically on a daily schedule.
[Unit]
Description=Daily System Backup Timer
Requires=system-backup.service
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1800
AccuracySec=1m
[Install]
WantedBy=timers.target
Enable and start the backup timer
Reload systemd configuration and enable the timer to start backups automatically.
sudo systemctl daemon-reload
sudo systemctl enable system-backup.timer
sudo systemctl start system-backup.timer
sudo systemctl status system-backup.timer
Configure log rotation
Set up logrotate to manage backup log files and prevent disk space issues.
/opt/backup/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
create 640 backup backup
postrotate
systemctl reload-or-restart rsyslog > /dev/null 2>&1 || true
endscript
}
Verify your setup
Test the backup system to ensure everything works correctly before relying on automated scheduling.
# Check timer status
sudo systemctl status system-backup.timer
List all active timers
sudo systemctl list-timers system-backup.timer
Test backup manually
sudo systemctl start system-backup.service
Check service status
sudo systemctl status system-backup.service
View backup logs
sudo tail -f /opt/backup/logs/backup.log
Check backup directory
sudo ls -la /opt/backup/local-backup/
Test SSH connection to remote server
sudo -u backup ssh backup-server "echo 'Connection successful'"
View systemd journal for backup service
journalctl -u system-backup.service -f
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Permission denied on backup directories | Incorrect ownership or permissions | Fix with sudo chown -R backup:backup /opt/backup and verify directory permissions are 750 |
| SSH connection fails to remote server | Key not authorized or network issues | Copy public key to remote server, test with sudo -u backup ssh backup-server |
| Timer not running backups | Timer not enabled or service failed | Check with sudo systemctl list-timers and enable with sudo systemctl enable system-backup.timer |
| Email notifications not sent | Postfix not configured properly | Configure Postfix with sudo dpkg-reconfigure postfix and test with echo "test" | mail admin@example.com |
| Backup script fails with rsync errors | Source directories don't exist or access denied | Check source paths in backup.conf and verify backup user has read access |
| Disk space full from old backups | Retention cleanup not working | Verify RETENTION_DAYS setting and manually clean with find /opt/backup/local-backup -mtime +30 -delete |
| Service fails to start | Systemd security restrictions too strict | Check journal with journalctl -u system-backup.service and adjust ReadWritePaths if needed |
Next steps
- Configure centralized logging with rsyslog and journald to improve backup monitoring
- Set up disk usage monitoring and cleanup to prevent backup storage issues
- Implement backup encryption with GPG for enhanced security
- Monitor backup performance with Prometheus and Grafana for production environments
- Optimize backup storage with compression and deduplication to reduce storage costs
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
BACKUP_EMAIL="admin@example.com"
REMOTE_HOST=""
REMOTE_USER="backup"
# Usage function
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -e EMAIL Email for notifications (default: admin@example.com)"
echo " -r HOST Remote backup server hostname/IP"
echo " -u USER Remote backup user (default: backup)"
echo " -h Show this help"
exit 1
}
# Parse arguments
while getopts "e:r:u:h" opt; do
case $opt in
e) BACKUP_EMAIL="$OPTARG" ;;
r) REMOTE_HOST="$OPTARG" ;;
u) REMOTE_USER="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
log_error "Installation failed. Cleaning up..."
userdel -r backup 2>/dev/null || true
rm -rf /opt/backup 2>/dev/null || true
}
# Set trap for cleanup on error
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Detect distribution
echo "[1/10] Detecting distribution..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
MAIL_PACKAGES="mailutils postfix"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
MAIL_PACKAGES="mailx postfix"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
MAIL_PACKAGES="mailx postfix"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
log_info "Detected $PRETTY_NAME"
# Update system packages
echo "[2/10] Updating system packages..."
$PKG_UPDATE
# Install required packages
echo "[3/10] Installing required packages..."
$PKG_INSTALL rsync openssh-clients $MAIL_PACKAGES
# Create backup user and directories
echo "[4/10] Creating backup user and directories..."
useradd --system --shell /bin/bash --home /opt/backup --create-home backup || true
mkdir -p /opt/backup/{scripts,logs,local-backup,.ssh}
chown -R backup:backup /opt/backup
chmod 755 /opt/backup
chmod 750 /opt/backup/{scripts,logs,local-backup}
chmod 700 /opt/backup/.ssh
# Generate SSH key
echo "[5/10] Generating SSH key for remote backups..."
sudo -u backup ssh-keygen -t ed25519 -f /opt/backup/.ssh/backup_key -N "" -C "backup@$(hostname)"
sudo -u backup chmod 600 /opt/backup/.ssh/backup_key
sudo -u backup chmod 644 /opt/backup/.ssh/backup_key.pub
# Create SSH config if remote host provided
if [[ -n "$REMOTE_HOST" ]]; then
echo "[6/10] Configuring SSH client settings..."
cat > /opt/backup/.ssh/config << EOF
Host backup-server
HostName $REMOTE_HOST
User $REMOTE_USER
Port 22
IdentityFile /opt/backup/.ssh/backup_key
StrictHostKeyChecking no
UserKnownHostsFile /opt/backup/.ssh/known_hosts
ServerAliveInterval 60
ServerAliveCountMax 3
Compression yes
EOF
chown backup:backup /opt/backup/.ssh/config
chmod 600 /opt/backup/.ssh/config
log_warn "Copy /opt/backup/.ssh/backup_key.pub to $REMOTE_HOST authorized_keys"
else
echo "[6/10] Skipping SSH config (no remote host specified)..."
fi
# Create backup configuration
echo "[7/10] Creating backup configuration..."
cat > /opt/backup/scripts/backup.conf << EOF
# Backup Configuration
BACKUP_NAME="\$(hostname)-system"
LOCAL_BACKUP_DIR="/opt/backup/local-backup"
REMOTE_BACKUP_HOST="backup-server"
REMOTE_BACKUP_DIR="/backup/servers/\$(hostname)"
LOG_FILE="/opt/backup/logs/backup.log"
RETENTION_DAYS=30
EMAIL_RECIPIENT="$BACKUP_EMAIL"
# Source directories to backup (space-separated)
BACKUP_SOURCES="/etc /var/log /home"
# Exclude patterns (space-separated)
EXCLUDE_PATTERNS="/var/log/*.log /var/log/journal/ /tmp/ /var/tmp/ /proc/ /sys/ /dev/ /run/*"
# Rsync options
RSYNC_OPTS="-avz --delete --delete-excluded --numeric-ids --hard-links"
EOF
chown backup:backup /opt/backup/scripts/backup.conf
chmod 640 /opt/backup/scripts/backup.conf
# Create main backup script
echo "[8/10] Creating backup script..."
cat > /opt/backup/scripts/backup.sh << 'EOF'
#!/bin/bash
set -euo pipefail
# Load configuration
source /opt/backup/scripts/backup.conf
# Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S'): $1" | tee -a "$LOG_FILE"
}
# Function to send email notification
send_notification() {
local subject="$1"
local message="$2"
echo "$message" | mail -s "[$(hostname)] $subject" "$EMAIL_RECIPIENT" 2>/dev/null || true
}
# Function to cleanup old backups
cleanup_old_backups() {
log_message "Cleaning up backups older than $RETENTION_DAYS days"
find "$LOCAL_BACKUP_DIR" -type d -name "$BACKUP_NAME-*" -mtime +$RETENTION_DAYS -exec rm -rf {} + 2>/dev/null || true
}
# Main backup function
perform_backup() {
local timestamp=$(date '+%Y%m%d_%H%M%S')
local backup_dir="$LOCAL_BACKUP_DIR/$BACKUP_NAME-$timestamp"
log_message "Starting backup to $backup_dir"
# Create backup directory
mkdir -p "$backup_dir"
# Build exclude options
local exclude_opts=""
for pattern in $EXCLUDE_PATTERNS; do
exclude_opts="$exclude_opts --exclude=$pattern"
done
# Perform local backup
for source in $BACKUP_SOURCES; do
if [[ -d "$source" ]]; then
log_message "Backing up $source"
rsync $RSYNC_OPTS $exclude_opts "$source/" "$backup_dir$(dirname $source)/" || {
log_message "ERROR: Failed to backup $source"
return 1
}
fi
done
# Create backup info file
cat > "$backup_dir/backup_info.txt" << EOL
Backup Date: $(date)
Hostname: $(hostname)
Backup Sources: $BACKUP_SOURCES
Total Size: $(du -sh "$backup_dir" | cut -f1)
EOL
log_message "Local backup completed successfully"
return 0
}
# Main execution
main() {
local start_time=$(date '+%Y-%m-%d %H:%M:%S')
if perform_backup; then
cleanup_old_backups
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local message="Backup completed successfully at $end_time (started at $start_time)"
log_message "$message"
send_notification "Backup Success" "$message"
exit 0
else
local message="Backup failed at $(date '+%Y-%m-%d %H:%M:%S')"
log_message "$message"
send_notification "Backup Failed" "$message"
exit 1
fi
}
main "$@"
EOF
chown backup:backup /opt/backup/scripts/backup.sh
chmod 750 /opt/backup/scripts/backup.sh
# Create systemd service
echo "[9/10] Creating systemd service and timer..."
cat > /etc/systemd/system/backup.service << EOF
[Unit]
Description=System Backup Service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/opt/backup/scripts/backup.sh
StandardOutput=journal
StandardError=journal
EOF
# Create systemd timer
cat > /etc/systemd/system/backup.timer << EOF
[Unit]
Description=Daily System Backup Timer
Requires=backup.service
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target
EOF
# Enable and start timer
systemctl daemon-reload
systemctl enable backup.timer
systemctl start backup.timer
# Final verification
echo "[10/10] Verifying installation..."
if systemctl is-active --quiet backup.timer; then
log_info "Backup timer is active"
else
log_error "Backup timer failed to start"
exit 1
fi
if [[ -x /opt/backup/scripts/backup.sh ]]; then
log_info "Backup script is executable"
else
log_error "Backup script is not executable"
exit 1
fi
# Clear trap
trap - ERR
log_info "Installation completed successfully!"
log_info "Backup timer status: $(systemctl is-active backup.timer)"
log_info "Next backup: $(systemctl list-timers backup.timer --no-pager | tail -n +2 | head -n 1 | awk '{print $1, $2}')"
log_info "Configuration file: /opt/backup/scripts/backup.conf"
log_info "Logs will be stored in: /opt/backup/logs/backup.log"
if [[ -n "$REMOTE_HOST" ]]; then
log_warn "Don't forget to copy the SSH public key to your remote server:"
echo " cat /opt/backup/.ssh/backup_key.pub"
fi
Review the script before running. Execute with: bash install.sh