Showing posts with label backup. Show all posts
Showing posts with label backup. Show all posts

02 August 2026

Sending ZFS backups to another server

After migrating back to my Lenovo server, it didn't have enough drive bays for my backup drives. So I needed to use the Dell as the backup destination.

Two servers:

  • Primary: this is the main server where the primary copy of the data exists (Lenovo)
  • Secondary: this is the backup server where we will send the data to a read-only copy (Dell)


Setup SSH

On the primary server, create ssh key pair (if you already have a key pair feel free to use that):

  • ssh-keygen -t ed25519 -C "root@primary"
    • -C is just a comment to allow for easier identification, so feel free to change, but make sure you also change where it is used later
  • cat id_ed25519.pub
    • We will need this output for saving the key


On the secondary server, create our backup user and give it the public key:

  • Install sudo
    • apt install sudo -y
  • Add the pveadmin user
    • adduser pveadmin
    • usermod -aG sudo pveadmin
    • # Allow pveadmin to run certain commands without prompting for password so use visudo to add the line
    • visudo
      • pveadmin ALL=(ALL) NOPASSWD: /usr/sbin/zfs list *, /usr/sbin/zfs receive *, /usr/sbin/zpool scrub *
  • Switch to the pveadmin user and add our ssh key
    • su - pveadmin
    • mkdir ~/.ssh
    • chmod 700 ~/.ssh
    • # Change the below to your key
    • echo "ssh-ed25519 ABCD1234... root@primary" >> ~/.ssh/authorized_keys
    • chmod 600 ~/.ssh/authorized_keys
    • exit
  • Test the ssh from primary to secondary and save the fingerprint
    • ssh pveadmin@secondary


Send the backups

  • Send over our initial data (use disown to prevent user disconnections from stopping job)
    • zfs send -R --raw rpool/data@backup-20260323 | ssh pveadmin@secondary "sudo zfs receive -o readonly=on backup-pool/data" 2>errors.log & disown
  • Send over incremental data
    • zfs send -R --raw -I rpool/data@backup-20260323 rpool/data@backup-20260401 | ssh pveadmin@secondary "sudo zfs receive backup-pool/data" 2>errors.log & disown


Automate the backups

  • Added a remote argument to my backup.sh script


Scripts

remote-backup.sh

#!/bin/sh

if [ $# -ne 3 ] ; then
  echo "Requires 3 args source, destination, and user@host"
  exit 1
fi

src=$1
dest=$2
remote=$3

snaplist=$(zfs list -t snapshot $src)
if [ $? -ne 0 ] ; then
  echo "No snapshots found for $src"
  exit 1
fi

bkpsnap=$(ssh ${remote} "sudo zfs list -t snapshot $dest 2>&1")
incremental=$?

if [ $incremental -eq 0 ] ; then
  incsnap=$(echo "$bkpsnap" | tail -n 1 | cut -f1 -d" " | cut -f2 -d"@")
  snap=$(echo "$snaplist" | grep "$incsnap" -A 1 | tail -n 1 | cut -f1 -d" " | cut -f2 -d"@")

  if [ "$incsnap" = "$snap" ] ; then
    echo "Backup is already up to date"
    exit 1
  fi

  echo "Sending incremental snap $src@$snap on top of $dest@$incsnap"
  echo "zfs send -R --raw -I $src@$incsnap $src@$snap | ssh ${remote} \"sudo zfs receive $dest\""
  zfs send -R --raw -I $src@$incsnap $src@$snap | ssh ${remote} "sudo zfs receive $dest"
else
  snap=$(echo "$snaplist" | tail -n +2 | head -n 1 | cut -f1 -d" " | cut -f2 -d"@")
  echo "Sending initial snap $src@$snap to $dest"
  echo "zfs send -R --raw $src@$snap | ssh ${remote} \"sudo zfs receive -o readonly=on $dest\""
  zfs send -R --raw $src@$snap | ssh ${remote} "sudo zfs receive -o readonly=on $dest"
fi

echo "Complete\n"


Appendix

Sources


07 September 2025

ZFS Backups

Here is the process that I use to create local zfs backups.


Manual Backup Procedure

  1. Create Snapshots
    • zfs snapshot -r storage/containers@backup-20240115
    • -r: for recursive
  2. Create a RaidZ1 Pool as our backup target
    • zpool create backup-pool raidz1 [DISK1] [DISK2] [DISK3] [DISK4]
  3. Send the initial snapshot
    • zfs send -R --raw storage/containers@backup-20240115 | zfs receive -o readonly=on backup-pool/containers
    • -R: for recursive
    • --raw: so that encrypted data is not decrypted (not encrypted data will remain not encrypted)
    • -o readonly=on: so that the backups are not editable
  4. Send incremental snapshots
    • zfs send -R --raw -I storage/containers@backup-20240115 storage/containers@backup-20240210 | zfs receive backup-pool/containers
    • -I [prev-snapshot]: incremental data since specified snapshot


Automating Backup Procedure

  1. Created the below scripts to help automate in /root/backup_scripts/
    • snapshot.sh
    • snapshot-all.sh
    • backup.sh
    • backup-all.sh
  2. Make them all executable
    • chmod +x *.sh
  3. Set the snapshot-all.sh script on crontab to run monthly
    • crontab -e
    • 05 8 1 * * sh /root/backup_scripts/snapshot-all.sh
    • remember that the cron time is UTC
  4. Manually push the backups
    • nohup sh backup-all.sh >>all.log 2>&1 &


Scripts

snapshot.sh

#!/bin/sh

DATASET=$1
zfs snapshot -r ${DATASET}@backup-$(date "+%Y%m%d")


snapshot-all.sh

#!/bin/sh

cd "$(dirname "$0")";
sh /root/backup_scripts/snapshot.sh rpool
sh /root/backup_scripts/snapshot.sh storage


backup.sh

#!/bin/sh

if [ $# -ne 2 ] ; then
  echo "Requires 2 args source and destination"
  exit 1
fi

src=$1
dest=$2

snaplist=$(zfs list -t snapshot $src)
if [ $? -ne 0 ] ; then
  echo "No snapshots found for $src"
  exit 1
fi

bkpsnap=$(zfs list -t snapshot $dest 2>&1)
incremental=$?

if [ $incremental -eq 0 ] ; then
  incsnap=$(echo "$bkpsnap" | tail -n 1 | cut -f1 -d" " | cut -f2 -d"@")
  snap=$(echo "$snaplist" | grep "$incsnap" -A 1 | tail -n 1 | cut -f1 -d" " | cut -f2 -d"@")

  if [ "$incsnap" = "$snap" ] ; then
    echo "Backup is already up to date"
    exit 1
  fi

  echo "Sending incremental snap $src@$snap on top of $dest@$incsnap"
  echo "zfs send -R --raw -I $src@$incsnap $src@$snap | zfs receive $dest"
  zfs send -R --raw -I $src@$incsnap $src@$snap | zfs receive $dest
else
  snap=$(echo "$snaplist" | tail -n +2 | head -n 1 | cut -f1 -d" " | cut -f2 -d"@")
  echo "Sending initial snap $src@$snap to $dest"
  echo "zfs send -R --raw $src@$snap | zfs receive -o readonly=on $dest"
  zfs send -R --raw $src@$snap | zfs receive -o readonly=on $dest
fi

echo "Complete\n"


backup-all.sh

#!/bin/sh

cd "$(dirname "$0")";

echo "========================================"
echo "Backup on $(date +"%Y-%m-%d %H:%M:%S")"
echo ""

zpool import bpool

sh /root/backup_scripts/backup.sh rpool/data backup-pool/data
sh /root/backup_scripts/backup.sh storage/containers backup-pool/containers
sh /root/backup_scripts/backup.sh storage/encrypted backup-pool/encrypted
sh /root/backup_scripts/backup.sh storage/lists backup-pool/lists

echo "Complete at $(date +"%Y-%m-%d %H:%M:%S")"

echo "Performing scrub"
zpool scrub -w backup-pool
echo "Complete at $(date +"%Y-%m-%d %H:%M:%S")"

zpool status backup-pool
zpool export backup-pool

echo "Spinning down hard drives"
hdparm -y /dev/disk/by-id/ata-WDC_WD60EFPX-68C5ZN0_WD-XXXXXXX
hdparm -y /dev/disk/by-id/ata-WDC_WD60EFPX-68C5ZN0_WD-XXXXXXX
hdparm -y /dev/disk/by-id/ata-WDC_WD60EFZX-68B3FN0_WD-XXXXXXX
hdparm -y /dev/disk/by-id/ata-WDC_WD60EFZX-68B3FN0_WD-XXXXXXX
echo "Hard drives spun down"
echo "========================================"


Appendix

Sources