Set up automated, deduplicated backups with BorgBackup and systemd timers for reliable system protection. This tutorial covers installation, repository setup with encryption, automated scheduling, and monitoring for production environments.
Prerequisites
- Root or sudo access
- At least 10GB free disk space for backups
- Basic familiarity with systemd and command line
What this solves
BorgBackup provides deduplicated, encrypted backups that save storage space and protect your data. Combined with systemd timers, you get reliable automated backups that run without cron dependencies and integrate with system logging for better monitoring.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest BorgBackup version.
sudo apt update && sudo apt upgrade -y
Install BorgBackup
Install BorgBackup from the official repositories. This provides the borg command for creating and managing backup repositories.
sudo apt install -y borgbackup
Create backup directory structure
Create directories for storing backup repositories and scripts. Use proper ownership and permissions for security.
sudo mkdir -p /opt/backups/repos
sudo mkdir -p /opt/backups/scripts
sudo mkdir -p /var/log/backups
Create backup user
Create a dedicated user for running backups. This follows security best practices by avoiding root execution.
sudo useradd --system --home /opt/backups --shell /bin/bash backup-user
sudo chown -R backup-user:backup-user /opt/backups
sudo chown backup-user:backup-user /var/log/backups
Initialize BorgBackup repository
Switch to the backup user and initialize an encrypted repository. The passphrase encrypts all backup data.
sudo -u backup-user bash
cd /opt/backups
export BORG_REPO=/opt/backups/repos/system-backup
export BORG_PASSPHRASE="YourSecurePassphrase123!"
borg init --encryption=repokey-blake2 $BORG_REPO
exit
Create backup script
Create the main backup script that defines what to backup, handles logging, and manages retention policies.
#!/bin/bash
BorgBackup automation script
Exit on any error
set -e
Configuration
export BORG_REPO="/opt/backups/repos/system-backup"
export BORG_PASSPHRASE="YourSecurePassphrase123!"
export BORG_RSH="ssh -i /home/backup-user/.ssh/id_rsa"
Logging
LOG_FILE="/var/log/backups/system-backup.log"
exec 1> >(tee -a "$LOG_FILE")
exec 2>&1
echo "========== Backup started: $(date) =========="
Backup paths to include
BACKUP_PATHS="
/etc
/home
/var/log
/opt
/root
/usr/local
"
Paths to exclude
EXCLUDE_PATTERNS="
--exclude /home/*/.cache
--exclude /home/*/.local/share/Trash
--exclude /var/log/journal
--exclude /var/cache
--exclude /tmp
--exclude '*.tmp'
--exclude '*.log.gz'
--exclude '.log.[0-9]'
--exclude 'lost+found'
"
Create backup with current timestamp
ARCHIVE_NAME="system-{now:%Y-%m-%d_%H:%M:%S}"
echo "Creating archive: $ARCHIVE_NAME"
borg create \
--verbose \
--filter AME \
--list \
--stats \
--show-rc \
--compression lz4 \
$EXCLUDE_PATTERNS \
"::$ARCHIVE_NAME" \
$BACKUP_PATHS
backup_exit=$?
echo "Backup completed with exit code: $backup_exit"
Prune old backups (retention policy)
echo "Pruning old archives"
borg prune \
--list \
--prefix system- \
--show-rc \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 1
prune_exit=$?
echo "Prune completed with exit code: $prune_exit"
Compact repository
echo "Compacting repository"
borg compact
compact_exit=$?
echo "Compact completed with exit code: $compact_exit"
Global exit status
global_exit=$(($backup_exit > $prune_exit ? $backup_exit : $prune_exit))
global_exit=$(($compact_exit > $global_exit ? $compact_exit : $global_exit))
if [ $global_exit -eq 0 ]; then
echo "========== Backup completed successfully: $(date) =========="
else
echo "========== Backup completed with errors: $(date) =========="
fi
echo ""
exit $global_exit
Set script permissions
Make the backup script executable and set secure permissions. Only the backup user should be able to read the script containing the passphrase.
sudo chmod 750 /opt/backups/scripts/system-backup.sh
sudo chown backup-user:backup-user /opt/backups/scripts/system-backup.sh
Create environment file
Store environment variables in a separate file for the systemd service. This keeps credentials separate from the script.
BORG_REPO=/opt/backups/repos/system-backup
BORG_PASSPHRASE=YourSecurePassphrase123!
PATH=/usr/local/bin:/usr/bin:/bin
Secure environment file
Set restrictive permissions on the environment file to protect credentials.
sudo chmod 600 /opt/backups/scripts/backup.env
sudo chown backup-user:backup-user /opt/backups/scripts/backup.env
Create systemd service unit
Define the systemd service that will execute the backup script. This runs as the backup user and loads the environment file.
[Unit]
Description=System Backup with BorgBackup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup-user
Group=backup-user
EnvironmentFile=/opt/backups/scripts/backup.env
ExecStart=/opt/backups/scripts/system-backup.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=system-backup
Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/opt/backups /var/log/backups
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
Create systemd timer unit
Create the timer that will trigger the backup service. This example runs backups daily at 2 AM with randomization to avoid system load spikes.
[Unit]
Description=Run system backup daily
Requires=system-backup.service
[Timer]
Run daily at 2 AM with 30 minute random delay
OnCalendar=daily
RandomizedDelaySec=30min
Persistent=true
Prevent multiple instances
AccuracySec=1h
[Install]
WantedBy=timers.target
Enable and start the timer
Reload systemd configuration and enable the timer to start automatically. The timer will persist across reboots.
sudo systemctl daemon-reload
sudo systemctl enable system-backup.timer
sudo systemctl start system-backup.timer
Create backup monitoring script
Create a script to monitor backup status and send alerts if backups fail or become outdated.
#!/bin/bash
Backup monitoring script
set -e
Configuration
export BORG_REPO="/opt/backups/repos/system-backup"
export BORG_PASSPHRASE="YourSecurePassphrase123!"
LOG_FILE="/var/log/backups/backup-monitor.log"
MAX_AGE_HOURS=26 # Alert if last backup is older than 26 hours
ALERT_EMAIL="admin@example.com"
Logging
exec 1> >(tee -a "$LOG_FILE")
exec 2>&1
echo "========== Backup Monitor Check: $(date) =========="
Get last backup info
LAST_ARCHIVE=$(borg list --short --last 1 | head -n1)
if [ -z "$LAST_ARCHIVE" ]; then
echo "ERROR: No backups found in repository"
echo "No backups found" | mail -s "Backup Alert: No backups found" $ALERT_EMAIL 2>/dev/null || echo "Failed to send email alert"
exit 1
fi
echo "Last backup archive: $LAST_ARCHIVE"
Check backup age
ARCHIVE_INFO=$(borg info "::$LAST_ARCHIVE" --json)
LAST_BACKUP_TIME=$(echo $ARCHIVE_INFO | python3 -c "import sys, json; print(json.load(sys.stdin)['archives'][0]['start'])")
LAST_BACKUP_TIMESTAMP=$(date -d "$LAST_BACKUP_TIME" +%s)
CURRENT_TIMESTAMP=$(date +%s)
AGE_HOURS=$(( (CURRENT_TIMESTAMP - LAST_BACKUP_TIMESTAMP) / 3600 ))
echo "Last backup was $AGE_HOURS hours ago"
if [ $AGE_HOURS -gt $MAX_AGE_HOURS ]; then
echo "WARNING: Last backup is older than $MAX_AGE_HOURS hours"
echo "Last backup was $AGE_HOURS hours ago" | mail -s "Backup Alert: Backup overdue" $ALERT_EMAIL 2>/dev/null || echo "Failed to send email alert"
exit 1
fi
Check repository integrity (weekly)
DAY_OF_WEEK=$(date +%u)
if [ $DAY_OF_WEEK -eq 7 ]; then # Sunday
echo "Running weekly repository check"
borg check --repository-only
if [ $? -eq 0 ]; then
echo "Repository integrity check passed"
else
echo "ERROR: Repository integrity check failed"
echo "Repository check failed" | mail -s "Backup Alert: Repository corrupted" $ALERT_EMAIL 2>/dev/null || echo "Failed to send email alert"
exit 1
fi
fi
echo "Backup monitoring completed successfully"
echo "========== Monitor Check Complete: $(date) ========="
echo ""
exit 0
Set monitoring script permissions
Make the monitoring script executable and secure.
sudo chmod 750 /opt/backups/scripts/backup-monitor.sh
sudo chown backup-user:backup-user /opt/backups/scripts/backup-monitor.sh
Create monitoring systemd units
Create systemd service and timer for backup monitoring that runs every 6 hours.
[Unit]
Description=Backup Monitoring Check
After=network-online.target
[Service]
Type=oneshot
User=backup-user
Group=backup-user
EnvironmentFile=/opt/backups/scripts/backup.env
ExecStart=/opt/backups/scripts/backup-monitor.sh
StandardOutput=journal
StandardError=journal
Create monitoring timer
Create the timer for backup monitoring checks.
[Unit]
Description=Run backup monitoring every 6 hours
Requires=backup-monitor.service
[Timer]
OnCalendar=--* 00,06,12,18:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable monitoring timer
Enable and start the monitoring timer.
sudo systemctl daemon-reload
sudo systemctl enable backup-monitor.timer
sudo systemctl start backup-monitor.timer
Create backup restore script
Create a utility script for easy backup restoration when needed.
#!/bin/bash
Backup restoration script
set -e
Configuration
export BORG_REPO="/opt/backups/repos/system-backup"
export BORG_PASSPHRASE="YourSecurePassphrase123!"
if [ $# -ne 2 ]; then
echo "Usage: $0 "
echo "Example: $0 system-2024-01-15_02:30:15 /tmp/restore"
echo ""
echo "Available archives:"
borg list --short
exit 1
fi
ARCHIVE_NAME="$1"
RESTORE_PATH="$2"
echo "Restoring archive '$ARCHIVE_NAME' to '$RESTORE_PATH'"
echo "This may take some time..."
Create restore directory
mkdir -p "$RESTORE_PATH"
Extract archive
borg extract --verbose "::$ARCHIVE_NAME" --destination "$RESTORE_PATH"
echo "Restore completed successfully"
echo "Files restored to: $RESTORE_PATH"
ls -la "$RESTORE_PATH"
Set restore script permissions
Make the restore script executable.
sudo chmod 750 /opt/backups/scripts/restore-backup.sh
sudo chown backup-user:backup-user /opt/backups/scripts/restore-backup.sh
Verify your setup
Check that all components are working correctly and view backup status.
# Check timer status
sudo systemctl status system-backup.timer
sudo systemctl status backup-monitor.timer
List active timers
sudo systemctl list-timers | grep backup
Run a test backup manually
sudo systemctl start system-backup.service
Check service logs
sudo journalctl -u system-backup.service -f
View backup repository info
sudo -u backup-user borg info /opt/backups/repos/system-backup
List available archives
sudo -u backup-user borg list /opt/backups/repos/system-backup
Advanced configuration
Configure remote backup storage
For production environments, consider backing up to remote storage. Here's how to configure SSH-based remote backups.
# Generate SSH key for backup user
sudo -u backup-user ssh-keygen -t rsa -b 4096 -f /opt/backups/.ssh/id_rsa -N ""
Copy public key to remote server
sudo -u backup-user ssh-copy-id backup@remote-server.example.com
Update backup script to use remote repository
export BORG_REPO="backup@remote-server.example.com:/backup/repos/system-backup"
Configure email notifications
Install mail utilities for backup alerts and notifications.
sudo apt install -y mailutils postfix
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| "Permission denied" errors | Incorrect file ownership or permissions | sudo chown -R backup-user:backup-user /opt/backups |
| "Repository not found" error | BORG_REPO path incorrect or not initialized | Check path and run borg init if needed |
| Timer not running | Timer not enabled or systemd not reloaded | sudo systemctl daemon-reload && sudo systemctl enable --now system-backup.timer |
| Backup fails with passphrase error | Wrong passphrase in environment file | Check /opt/backups/scripts/backup.env |
| Large log files filling disk | No log rotation configured | Configure logrotate for /var/log/backups/ |
| Backup takes too long | Too many files or wrong compression | Adjust exclude patterns or use faster compression |
Backup retention and monitoring
Understanding and managing your backup retention policy is crucial for effective backup management. The configured retention policy keeps:
- Daily backups for 7 days
- Weekly backups for 4 weeks
- Monthly backups for 6 months
- Yearly backups for 1 year
Monitor disk usage regularly and adjust retention as needed. Use the monitoring script to ensure backups are running successfully and check for corruption weekly.
For comprehensive backup monitoring in production environments, consider integrating with system performance monitoring tools to track backup performance and resource usage.
Next steps
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' # No Color
# Configuration
BACKUP_USER="backup-user"
BACKUP_HOME="/opt/backups"
REPO_DIR="$BACKUP_HOME/repos"
SCRIPTS_DIR="$BACKUP_HOME/scripts"
LOG_DIR="/var/log/backups"
SERVICE_NAME="system-backup"
# Error handling
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Rolling back changes...${NC}"
systemctl --quiet is-enabled "$SERVICE_NAME.timer" 2>/dev/null && systemctl disable "$SERVICE_NAME.timer" || true
systemctl --quiet is-active "$SERVICE_NAME.timer" 2>/dev/null && systemctl stop "$SERVICE_NAME.timer" || true
rm -f "/etc/systemd/system/$SERVICE_NAME.service" "/etc/systemd/system/$SERVICE_NAME.timer"
userdel "$BACKUP_USER" 2>/dev/null || true
rm -rf "$BACKUP_HOME" "$LOG_DIR"
systemctl daemon-reload
exit 1
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Usage
usage() {
echo "Usage: $0 <passphrase> [backup_schedule]"
echo " passphrase: Encryption passphrase for BorgBackup repository"
echo " backup_schedule: systemd timer format (default: daily)"
echo ""
echo "Examples:"
echo " $0 'MySecurePassphrase123!'"
echo " $0 'MySecurePassphrase123!' '*-*-* 02:00:00'"
exit 1
}
# Validate arguments
if [[ $# -lt 1 ]] || [[ $# -gt 2 ]]; then
usage
fi
BORG_PASSPHRASE="$1"
BACKUP_SCHEDULE="${2:-*-*-* 02:00:00}"
if [[ ${#BORG_PASSPHRASE} -lt 12 ]]; then
echo -e "${RED}Passphrase must be at least 12 characters long${NC}"
exit 1
fi
echo -e "${GREEN}Starting BorgBackup installation and configuration...${NC}"
# Detect distro
echo -e "${YELLOW}[1/10] Detecting distribution...${NC}"
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"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
# Update system packages
echo -e "${YELLOW}[2/10] Updating system packages...${NC}"
$PKG_UPDATE
# Install BorgBackup
echo -e "${YELLOW}[3/10] Installing BorgBackup...${NC}"
$PKG_INSTALL borgbackup
# Create directory structure
echo -e "${YELLOW}[4/10] Creating backup directory structure...${NC}"
mkdir -p "$REPO_DIR" "$SCRIPTS_DIR" "$LOG_DIR"
# Create backup user
echo -e "${YELLOW}[5/10] Creating backup user...${NC}"
useradd --system --home "$BACKUP_HOME" --shell /bin/bash "$BACKUP_USER" || true
chown -R "$BACKUP_USER:$BACKUP_USER" "$BACKUP_HOME"
chown "$BACKUP_USER:$BACKUP_USER" "$LOG_DIR"
# Initialize BorgBackup repository
echo -e "${YELLOW}[6/10] Initializing BorgBackup repository...${NC}"
sudo -u "$BACKUP_USER" bash -c "
export BORG_REPO='$REPO_DIR/system-backup'
export BORG_PASSPHRASE='$BORG_PASSPHRASE'
borg init --encryption=repokey-blake2 \$BORG_REPO
"
# Create backup script
echo -e "${YELLOW}[7/10] Creating backup script...${NC}"
cat > "$SCRIPTS_DIR/backup.sh" << 'EOF'
#!/bin/bash
set -e
export BORG_REPO="/opt/backups/repos/system-backup"
export BORG_PASSPHRASE="PASSPHRASE_PLACEHOLDER"
LOG_FILE="/var/log/backups/system-backup.log"
exec 1> >(tee -a "$LOG_FILE")
exec 2>&1
echo "========== Backup started: $(date) =========="
BACKUP_PATHS="/etc /home /var/log /opt /root /usr/local"
EXCLUDE_PATTERNS="
--exclude /home/*/.cache
--exclude /home/*/.local/share/Trash
--exclude /var/log/journal
--exclude /var/cache
--exclude /tmp
--exclude '*.tmp'
--exclude '*.log.gz'
--exclude '*.log.[0-9]*'
--exclude 'lost+found'
"
ARCHIVE_NAME="system-{now:%Y-%m-%d_%H:%M:%S}"
echo "Creating archive: $ARCHIVE_NAME"
borg create \
--verbose \
--filter AME \
--list \
--stats \
--show-rc \
--compression lz4 \
$EXCLUDE_PATTERNS \
"::$ARCHIVE_NAME" \
$BACKUP_PATHS
backup_exit=$?
echo "Pruning old archives"
borg prune \
--list \
--prefix system- \
--show-rc \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 1
prune_exit=$?
echo "Compacting repository"
borg compact
compact_exit=$?
global_exit=$((backup_exit > prune_exit ? backup_exit : prune_exit))
global_exit=$((compact_exit > global_exit ? compact_exit : global_exit))
if [ $global_exit -eq 0 ]; then
echo "========== Backup completed successfully: $(date) =========="
else
echo "========== Backup completed with errors: $(date) =========="
fi
exit $global_exit
EOF
# Replace passphrase placeholder
sed -i "s/PASSPHRASE_PLACEHOLDER/$BORG_PASSPHRASE/g" "$SCRIPTS_DIR/backup.sh"
# Set script permissions
chmod 750 "$SCRIPTS_DIR/backup.sh"
chown "$BACKUP_USER:$BACKUP_USER" "$SCRIPTS_DIR/backup.sh"
# Create systemd service
echo -e "${YELLOW}[8/10] Creating systemd service...${NC}"
cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF
[Unit]
Description=System Backup with BorgBackup
After=network.target
[Service]
Type=oneshot
User=$BACKUP_USER
Group=$BACKUP_USER
ExecStart=$SCRIPTS_DIR/backup.sh
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Create systemd timer
cat > "/etc/systemd/system/$SERVICE_NAME.timer" << EOF
[Unit]
Description=Run system backup daily
Requires=$SERVICE_NAME.service
[Timer]
OnCalendar=$BACKUP_SCHEDULE
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Enable and start systemd timer
echo -e "${YELLOW}[9/10] Enabling systemd timer...${NC}"
systemctl daemon-reload
systemctl enable "$SERVICE_NAME.timer"
systemctl start "$SERVICE_NAME.timer"
# Verification
echo -e "${YELLOW}[10/10] Running verification checks...${NC}"
# Check if backup user exists
if id "$BACKUP_USER" &>/dev/null; then
echo -e "${GREEN}✓ Backup user created successfully${NC}"
else
echo -e "${RED}✗ Backup user creation failed${NC}"
exit 1
fi
# Check if repository exists
if sudo -u "$BACKUP_USER" bash -c "export BORG_REPO='$REPO_DIR/system-backup'; export BORG_PASSPHRASE='$BORG_PASSPHRASE'; borg info" &>/dev/null; then
echo -e "${GREEN}✓ BorgBackup repository initialized successfully${NC}"
else
echo -e "${RED}✗ BorgBackup repository initialization failed${NC}"
exit 1
fi
# Check if systemd timer is active
if systemctl is-active --quiet "$SERVICE_NAME.timer"; then
echo -e "${GREEN}✓ Systemd timer is active${NC}"
else
echo -e "${RED}✗ Systemd timer is not active${NC}"
exit 1
fi
# Check if systemd timer is enabled
if systemctl is-enabled --quiet "$SERVICE_NAME.timer"; then
echo -e "${GREEN}✓ Systemd timer is enabled${NC}"
else
echo -e "${RED}✗ Systemd timer is not enabled${NC}"
exit 1
fi
echo -e "${GREEN}BorgBackup installation and configuration completed successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Store your passphrase securely: $BORG_PASSPHRASE"
echo "2. Check timer status: systemctl status $SERVICE_NAME.timer"
echo "3. View logs: journalctl -u $SERVICE_NAME.timer"
echo "4. Manual backup test: sudo -u $BACKUP_USER $SCRIPTS_DIR/backup.sh"
echo "5. List backups: sudo -u $BACKUP_USER bash -c 'export BORG_REPO=$REPO_DIR/system-backup; export BORG_PASSPHRASE=\"$BORG_PASSPHRASE\"; borg list'"
Review the script before running. Execute with: bash install.sh