blue-ox.nl From coffee-fueled fruity tech to fast runs—think different, let’s run them.

Raspberry Pi backup rsync (SSH) based

R

Backup your complete Pi to a Synology NAS

Create ED25519 keys on RPi

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rpi-backup -C "rpi-backup"Code language: JavaScript (javascript)

From your user home directory

Take care your .ssh folder is readable

chmod 700 ~/.ssh
chmod 700 ~/.ssh
cd .sshCode language: CSS (css)
nano config

Enter the following (creating an alias with all kinds of presets to the SSH connection)

Host nas
    HostName 192.168.1.3
    User <USER-NAME>
    Port 2222 #SSH-port of your Synology NAS
    IdentityFile ~/.ssh/id_ed25519_rpi-backup
    ConnectTimeout 10
    ServerAliveInterval 60
    ServerAliveCountMax 3Code language: PHP (php)

Je configuratiebestand:

Host nas
HostName 192.168.1.3
User Erik
Port 2222
IdentityFile /home/erik/.ssh/id_ed25519_rpi-media
ConnectTimeout 10
ServerAliveInterval 60
ServerAliveCountMax 3

Analyse:

  1. Host nas:
    • Dit stelt een alias in voor het adres van je NAS. Je kunt nu eenvoudig ssh nas typen in plaats van ssh -p 2222 Erik@192.168.1.3.
  2. HostName 192.168.1.3:
    • Dit is correct. Het verwijst naar het IP-adres van je NAS.
  3. User Erik:
    • Zorg ervoor dat de gebruiker Erik bestaat op je NAS en dat SSH voor deze gebruiker is geconfigureerd.
  4. Port 2222:
    • Dit lijkt correct, ervan uitgaande dat je NAS geconfigureerd is om SSH-verkeer te accepteren op poort 2222.
  5. IdentityFile /home/erik/.ssh/id_ed25519_rpi-media:
    • Controleer of dit bestand bestaat, goed geconfigureerd is en de juiste permissies heeft:bashKopiërenBewerkenls -l /home/erik/.ssh/id_ed25519_rpi-media Het bestand moet de permissie -rw------- hebben (600).
  6. ConnectTimeout 10:
    • Dit bepaalt hoe lang de SSH-client wacht bij het maken van een verbinding. 10 seconden is prima.
  7. ServerAliveInterval 60 en ServerAliveCountMax 3:
    • Deze instellingen zorgen ervoor dat de verbinding actief blijft. Na 3 minuten (60 seconden x 3) zonder activiteit wordt de verbinding gesloten. Dit is een goede instelling.
cat ~/.ssh/id_ed25519_rpi-backup.pubCode language: JavaScript (javascript)

copy de line which looks something like,

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH... rpi-backup

SSH into your Synology NAS, watch it in the next line! And take care of being at your users home folder

The > means, empty the existing file and replace with ….
The >> means, add at the and of the existing file …..

echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH... rpi-backup" <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-typology-acc-color">>></mark> .ssh/authorized_keysCode language: JavaScript (javascript)
chmod 600 ~/.ssh/authorized_keysCode language: JavaScript (javascript)

Testing,

ssh nas
#!/bin/bash
set -e
set -u
#echo "Starting script"
#set -x  # Print commands as they execute
#echo "After set commands"


# Configuration
HOSTNAME=$(hostname)
LOG="/var/log/rpi-system-backup.log"
LOG_MAX_SIZE=10M
LOG_BACKUP_COUNT=7
LOCK_TIMEOUT=3600 # 1 hour timeout for lock file
REMOTE_PATH="/volume1/RPi-archive/${HOSTNAME}"

# Backup exclusions
EXCLUDE_LIST=(
    "/proc"
    "/sys"
    "/dev"
    "/tmp"
    "/run"
    "/lost+found"
    "/var/log"
    "/var/cache/apt/archives"
    "/home/*/.cache"
    "/media"
    "/mnt"
    "/var/lib/docker/overlay2"
    "/var/lib/docker/containers"
)

# Services to pause during backup
CRITICAL_SERVICES=(
    "mariadb"
    "postgres"
)

# Docker containers to pause
CRITICAL_CONTAINERS=(
    "immich_postgres"
    "immich_redis"
    "immich_server"
)

# Retention configuration
DAILY_RETENTION=7
WEEKLY_RETENTION=4
MONTHLY_RETENTION=12
YEARLY_RETENTION=2

# Lock file
LOCK_FILE="/tmp/rpi_system_backup.lock"

# Logging function
log_message() {
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "$timestamp - $1" | tee -a "$LOG"
}

LOG_MAX_SIZE=10M
LOG_BACKUP_COUNT=7

rotate_logs() {
   if [ -f "$LOG" ]; then
       size=$(stat -f%z "$LOG" 2>/dev/null || stat -c%s "$LOG" 2>/dev/null)
       max_size=$(numfmt --from=iec $LOG_MAX_SIZE)

       if [ "$size" -gt "$max_size" ]; then
           for ((i=LOG_BACKUP_COUNT-1; i>=1; i--)); do
               [ -f "$LOG.$i" ] && mv "$LOG.$i" "$LOG.$((i+1))"
           done
           mv "$LOG" "$LOG.1"
           touch "$LOG"
       fi
   fi
}

# Enhanced lock management with timeout
check_lock() {
   if [ -f "$LOCK_FILE" ]; then
       pid=$(cat "$LOCK_FILE")
       if kill -0 "$pid" 2>/dev/null; then
           lock_age=$(($(date +%s) - $(stat -c %Y "$LOCK_FILE")))
           if [ $lock_age -gt $LOCK_TIMEOUT ]; then
               log_message "Lock file is stale (age: ${lock_age}s). Removing."
               rm -f "$LOCK_FILE"
           else
               log_message "Another backup process is running (PID: $pid)"
               exit 1
           fi
       fi
   fi
   echo $$ > "$LOCK_FILE"
}

cleanup() {
    rm -f "$LOCK_FILE"
    log_message "Cleanup completed"
}

trap cleanup EXIT INT TERM

# Check requirements
check_tools() {
    for tool in rsync ssh docker; do
        if ! command -v $tool &> /dev/null; then
            log_message "ERROR: $tool is not installed."
            exit 1
        fi
    done
}

# Create backup directories
create_backup_structure() {
    log_message "Creating backup directory structure"
    if ! ssh root "
        mkdir -p '${REMOTE_PATH}/current' \
                '${REMOTE_PATH}/daily' \
                '${REMOTE_PATH}/weekly' \
                '${REMOTE_PATH}/monthly' \
                '${REMOTE_PATH}/yearly'
    "; then
        log_message "Failed to create backup structure"
        exit 1
    fi
}

# Generate exclude parameters
generate_exclude_params() {
    local exclude_params=""
    for item in "${EXCLUDE_LIST[@]}"; do
        exclude_params+="--exclude=${item} "
    done
    echo "$exclude_params"
}

# Check available space
check_space() {
    local required_space=$(du -sx --exclude=/proc --exclude=/sys --exclude=/dev / 2>/dev/null | awk '{print $1}')
    local available_space

    if ! available_space=$(ssh root \
        "df -k '${REMOTE_PATH}' | tail -1 | awk '{print \$4}'"); then
        log_message "ERROR: Failed to check remote space"
        return 1
    fi

    if [ -z "$available_space" ] || [ -z "$required_space" ]; then
        log_message "ERROR: Failed to determine space requirements"
        return 1
    fi

    if [ "$available_space" -lt "$required_space" ]; then
        log_message "ERROR: Insufficient space. Required: ${required_space}KB, Available: ${available_space}KB"
        return 1
    fi
    return 0
}

# Handle services
handle_services() {
    local action=$1
    local failed=0

    log_message "${action^}ing critical services and containers"

    # System services
    for service in "${CRITICAL_SERVICES[@]}"; do
        if systemctl is-active --quiet "$service"; then
            if ! systemctl "$action" "$service"; then
                log_message "Failed to $action $service"
                failed=1
            fi
        fi
    done

    # Docker containers
    if command -v docker >/dev/null 2>&1; then
        for container in "${CRITICAL_CONTAINERS[@]}"; do
            if docker ps -q -f name="$container" >/dev/null; then
                if [ "$action" = "stop" ]; then
                    if ! docker inspect --format '{{.State.Paused}}' "$container" | grep -q "true"; then
                        docker pause "$container" || failed=1
                    fi
                else
                    if docker inspect --format '{{.State.Paused}}' "$container" | grep -q "true"; then
                        docker unpause "$container" || failed=1
                    fi
                fi
            fi
        done
    fi

    return $failed
}

# Backup rotation
rotate_backups() {
    local date_suffix=$(date +%Y%m%d)
    local daily_backup="${REMOTE_PATH}/daily/backup_${date_suffix}"

    if ssh root "[ -d '$daily_backup' ]"; then
        log_message "Daily backup for ${date_suffix} already exists, skipping rotation"
        return 0
    fi

    ssh root "
        cd '${REMOTE_PATH}/daily' && ls -1t | tail -n +$((DAILY_RETENTION + 1)) | xargs -r rm -rf;
        cd '${REMOTE_PATH}/weekly' && ls -1t | tail -n +$((WEEKLY_RETENTION + 1)) | xargs -r rm -rf;
        cd '${REMOTE_PATH}/monthly' && ls -1t | tail -n +$((MONTHLY_RETENTION + 1)) | xargs -r rm -rf;
        cd '${REMOTE_PATH}/yearly' && ls -1t | tail -n +$((YEARLY_RETENTION + 1)) | xargs -r rm -rf
    "
    sleep 5

    ssh root "cp -al '${REMOTE_PATH}/current' '${REMOTE_PATH}/daily/backup_${date_suffix}'"
}

# Cleanup old backups
cleanup_old_backups() {
    log_message "Starting cleanup of old backups"

    local cleanup_commands="
        find '${REMOTE_PATH}/daily' -maxdepth 1 -type d -mtime +${DAILY_RETENTION} -exec rm -rf {} \;
        find '${REMOTE_PATH}/weekly' -maxdepth 1 -type d -mtime +$((WEEKLY_RETENTION * 7)) -exec rm -rf {} \;
        find '${REMOTE_PATH}/monthly' -maxdepth 1 -type d -mtime +$((MONTHLY_RETENTION * 30)) -exec rm -rf {} \;
        find '${REMOTE_PATH}/yearly' -maxdepth 1 -type d -mtime +$((YEARLY_RETENTION * 365)) -exec rm -rf {} \;
    "

    if ! ssh root "$cleanup_commands"; then
        log_message "Failed to cleanup old backups"
        return 1
    fi

    log_message "Cleanup of old backups completed"
    return 0
}

# Define rsync options
RSYNC_OPTS="-aAX --delete --timeout=120 --no-specials --copy-unsafe-links --partial --quiet --no-acls"
DRY_RUN=false

for arg in "$@"; do
    case $arg in
        --dry-run)
            DRY_RUN=true
            ;;
    esac
done

if $DRY_RUN; then
    RSYNC_OPTS+=" --dry-run"
fi

# Perform backup
perform_backup() {
    local exclude_params=$(generate_exclude_params)
    local rsync_output=$(mktemp)

    nice -n 19 ionice -c2 -n7 sudo rsync $RSYNC_OPTS \
        $exclude_params \
        / "root:${REMOTE_PATH}/current/" \
        --rsync-path="/bin/rsync" 2>"$rsync_output"

    local status=$?
    if [ $status -ne 0 ]; then
        log_message "ERROR: Backup failed (code $status): $(cat $rsync_output)"
        rm "$rsync_output"
        return 1
    fi
    rm "$rsync_output"
    return 0
}

# Main execution
main() {
   log_message "Starting backup process"
   rotate_logs
   check_lock
   check_tools
   create_backup_structure

   if ! check_space; then
       log_message "Space check failed"
       exit 1
   fi

   if ! handle_services stop; then
       log_message "Failed to stop services"
       handle_services start
       exit 1
   fi

   if perform_backup; then
       if rotate_backups; then
           log_message "Backup and rotation completed successfully"
       else
           log_message "Backup succeeded but rotation failed"
           exit 1
       fi
   else
       log_message "Backup failed"
       exit 1
   fi

   if ! handle_services start; then
       log_message "Failed to restart services"
       exit 1
   fi
}

# Run main function
mainCode language: PHP (php)

Met deze instellingen zal je Pi niet vastlopen als de NFS share niet beschikbaar is tijdens het opstarten. door de combinatie van noauto met x-systemd.automount zal het systeem de share automatisch mounten zodra er toegang toe nodig is.

Het werkt zo:

  • noauto: voorkomt dat het direct bij boot gemount wordt
  • x-systemd.automount: zorgt ervoor dat systemd een “automount point” creëert dat de share automatisch mount zodra er toegang toe nodig is

Zonder x-systemd.automount zou je inderdaad gelijk hebben – dan zou noauto betekenen dat je het handmatig moet mounten. Maar deze combinatie geeft je het beste van beide werelden:

Robuuste failback als de share tijdelijk onbereikbaar is

Geen vertraging tijdens boot als de share niet bereikbaar is

Automatisch mounten zodra je het nodig hebt

192.168.xx.xx:/volume1/raspiBackup /mnt/backup nfs noauto,x-systemd.automount,soft,timeo=15,retrans=2 0 0Code language: JavaScript (javascript)
sudo systemctl daemon-reload
sudo systemctl restart remote-fs.targetCode language: CSS (css)

Get and install the RaspiBackup application, walk through the options and configure as you want.

Watch it, this is the right one (look at the difference hyperlinks the two sources provided)

sudo curl -o install -L https://raspibackup.linux-tips-and-tricks.de/install; sudo bash ./installCode language: JavaScript (javascript)

Start the backup for the first time.

sudo raspiBackup -m detailed

And to take care of automatic backup, check via the next settings/configuration file if it is of your liking (and as how you entered it during installation)

sudo nano /etc/systemd/system/raspiBackup.timer
[Unit]
Description=Timer for raspiBackup.service to start backup

[Timer]
OnCalendar=*-*-* 02:00:42
# Create a backup every day at 02:00 and 42 seconds              
Unit=raspiBackup.service

[Install]
WantedBy=multi-user.targetCode language: PHP (php)

After you made changes you have to tell the system t reload the service again

sudo systemctl daemon-reload
sudo systemctl enable raspiBackup.timer
sudo systemctl start raspiBackup.timerCode language: CSS (css)

sources (references) & credits

Basically all input/information came from the websites below. So credits and thanks to those content creators and subject matter experts. The only reason I mainly copy/paste their content is to guarantee I have a backup for myself and because multiple times I had to change and adapt. So archiving the “scripts” as I executed it succesfully is inportant for me.

https://boldt.blog/rasperry-pi-backup-to-synology-nas-with-raspibackup/
https://www.linux-tips-and-tricks.de/en/installation
https://pimylifeup.com/cron-jobs-and-crontab/
https://claude.ai

About the author

Add comment

By Erik
blue-ox.nl From coffee-fueled fruity tech to fast runs—think different, let’s run them.

Pages

Tags