HOWTO: Backup script for external USB Drives that are hot pluggable

Hi all,

I searched quite a while for a solution to backup parts of my Truenas storage contents to external USB drives.

  • I need external USB drives to be able to carry them around (offsite storage)
  • I wanted the possibility to hot-swap them to easily exchange a pool of USB drives (currently I use 3 different drives)
  • I just want to exchange the drives without changing anything in the config or initiate anything in the webui
  • The backup should run on a weekly cycle (Disk 1 Tue, Disk 2 Thu, Disk 3 Sun)

As I couldn’t find one I wrote a script myself and after having it run some time I thought it might be useful for the one or another :wink:

The script backups the Truenas config and any number of folders you specify to the USB drives.

If you have any suggestions on how to make it more useful or improve it, do let me know.

You need:

  • at least one external USB drive
  • be able format the drive in ext4 file format (issues with windows?!)
  • basic knowledge on how to use the command line and truenas webui

Caveat:
I first saved the script in /mnt/ but it apperently got deleted after a reboot/powerloss so I placed it in home. So now root is executing a script from the truenas_admin home folder! Maybe someone has a better solution or another folder that might be more suitable.

Steps to set up the backup script:

  1. Format your external USB Drive(s) with ext4 file system.
    Linux: I used Gparted.
    Windows: You need an application for this.

  2. After formatting your USB drive(s) get the UUID(s) by entering in your console and copy it:

Linux: sudo blkid -sUUID

Windows: I don’t know how to do it there maybe someone can help. I tried the below but it gave me another UUID compared to Linux :frowning:
Type “diskpart” and press Enter. Next, type list disk to see a list of connected disks. Identify your USB drive by its disk number. Finally, type “select disk ” (replace with the actual disk number). Then enter “UNIQUEID DISK”

  1. Copy my script to your local file editor and edit the “SOURCEFOLDERS=” line.
    My setup: SOURCEFOLDERS=(“/mnt/Data/Dokumente” “/mnt/Data/Bilder” “/mnt/Data/Backup” “/mnt/Data/Syncthing”)

You can just edit/remove/add those folders as you need them to be backuped

The exact folders can be found in your Truenas webui under “Datasets” - click a dataset and then look at the “Dataset Details” section. There should be written “Path: …” (in my case one is “Path: Data/Dokumente”). You can copy it by clicking on the “Copy to Clipboard”-icon right next to it. Make sure the “/mnt/” path is not changed (in my case it would be “/mnt/Data/Dokumente”).

  1. When you have inserted/edited your folders, access the Truenas Webui and go to “System” - “Services” and start the “SSH” service by clicking in the “Running” coloumn.

  2. Go to your operating system console and login truenas via the ssh access:

ssh truenas_admin@192.168.178.13
Enter truenas_admin password
You will be in the /home/truenas_admin/ folder by default

Backspace might not work in the console so enter the commands carefully!

  1. Create a script by using the following command (open with file editor Vim):
    vim ./rsync_backup.sh
    Wait till Vim opened, then
    press “i” for insert mode
    copy the code of the script into the file (copy the edited code from your local file editor into the clipboard then insert it with Ctrl+Shift+v into Vim)
    press ESC for normal mode
    enter “:”
    enter “wq!”

  2. make the script executable by typing:
    sudo chmod +x ./rsync_backup.sh

  3. Go back to your Truenas webui and go to “System” - “Advanced Settings” - “Cron Jobs” - click “Add”
    Enter the following information:
    Description: Rsync to usb disk 1
    Command: bash /home/truenas_admin/rsync_backup.sh 505382a2-2768-4e59-be75-68abfd162797
    (For the command replace the UUID that you have extracted from your external USB Drive at step 2.)
    Run As User: root
    Schedule: select as needed
    Options: Tick the two boxes “Hide Standard Output” and “Enabled”
    Click “Save”

  4. Now you can test run your Backup job by clicking the “Run Job” Button next to your newly created Cron Job.

  5. Repeat step 8 for any additional disk you would like to add to the backup cycle.

My Backup Script:

#!/bin/bash

inputUUID="$1"

#Configuration
LOGDATE=$(date +"%Y%m%d-%H%M%S")
LOGNAME=$LOGDATE"_"$inputUUID
LOGFOLDER="/mnt/Data/Dokumente/backup_logs"
SOURCEFOLDERS=("/mnt/Data/Dokumente" "/mnt/Data/Bilder" "/mnt/Data/Backup" "/mnt/Data/Syncthing")
DESTFOLDER="/mnt/backupdisk"

isUuidMounted() { findmnt --source UUID="$1" >/dev/null;} #UUID only
mkdir -p /mnt/backupdisk
mount UUID=$inputUUID /mnt/backupdisk

mkdir -p $LOGFOLDER
LOGFILE=$LOGFOLDER"/"$LOGNAME".log"
exec 3>&1 1>"$LOGFILE" 2>&1

echo "Start: "$(date -R)
echo "UUID:  "$inputUUID
echo "#############################################################"
echo

if isUuidMounted $inputUUID;
  then
    echo   "Disk is mounted"
    echo
    echo "#############################################################"
    echo "Truenas Configuration Backup"
    echo "#############################################################"
    echo
    mkdir -p $DESTFOLDER"/truenas_config"
    rsync -rltgoDv /var/db/system/configs-*/ $DESTFOLDER"/truenas_config"

    for FOLDER in "${SOURCEFOLDERS[@]}"
    do
      echo
      echo "#############################################################"
      echo "Sourcefolder: " $FOLDER
      echo "#############################################################"
      echo
      mkdir -p $FOLDER
      rsync -rltgoDv $FOLDER $DESTFOLDER
    done
  else
    echo   "Disk is not mounted"
fi

umount /mnt/backupdisk

echo
echo "#############################################################"
echo "End: "$(date -R)
echo "#############################################################"
2 Likes

I just saw I missed the automatic creation of the Logfolder and can’t edit my initial post?!

Anyway here is the corrected version:

#!/bin/bash

inputUUID="$1"

#Configuration
LOGDATE=$(date +"%Y%m%d-%H%M%S")
LOGNAME=$LOGDATE"_"$inputUUID
LOGFOLDER="/mnt/Data/Dokumente/backup_logs"
SOURCEFOLDERS=("/mnt/Data/Dokumente" "/mnt/Data/Bilder" "/mnt/Data/Backup" "/mnt/Data/Syncthing")
DESTFOLDER="/mnt/backupdisk"

isUuidMounted() { findmnt --source UUID="$1" >/dev/null;} #UUID only
mkdir -p $LOGFOLDER
mkdir -p /mnt/backupdisk
mount UUID=$inputUUID /mnt/backupdisk

mkdir -p $LOGFOLDER
LOGFILE=$LOGFOLDER"/"$LOGNAME".log"
exec 3>&1 1>"$LOGFILE" 2>&1

echo "Start: "$(date -R)
echo "UUID:  "$inputUUID
echo "#############################################################"
echo

if isUuidMounted $inputUUID;
  then
    echo   "Disk is mounted"
    echo
    echo "#############################################################"
    echo "Truenas Configuration Backup"
    echo "#############################################################"
    echo
    mkdir -p $DESTFOLDER"/truenas_config"
    rsync -rltgoDv /var/db/system/configs-*/ $DESTFOLDER"/truenas_config"

    for FOLDER in "${SOURCEFOLDERS[@]}"
    do
      echo
      echo "#############################################################"
      echo "Sourcefolder: " $FOLDER
      echo "#############################################################"
      echo
      mkdir -p $FOLDER
      rsync -rltgoDv $FOLDER $DESTFOLDER
    done
  else
    echo   "Disk is not mounted"
fi

umount /mnt/backupdisk

echo
echo "#############################################################"
echo "End: "$(date -R)
echo "#############################################################"
1 Like

I’m write in german, because my english is bad.

Tolle Idee, möglicherweise geht es viel einfacher. Rsync job einrichten, fixen mountpoint als destination z.B. /mnt/backupdisk hinterlegen, fertig.

Zuvor habe ich für Umbrel+Start9 was ähnliches gebastelt. Die blockchain sollte rotierend auf verschiedene Disks gesichert werden. Hat gut funktioniert. Nur eine Routine für die Autoerkennung der Disks habe ich nie fertiggestellt (Zeile 19-32).

Hier mein (sub)script…

#!/bin/bash
# backup blockchain to USB disk

set -e
echo "-------------------------------------------------------------------------------"
echo "Backup blockchain to external drive"

if [ ! $bakservice ]; then
	echo "[ ! ] Attention: Script direct started, the services are RUNNING and files are OPEN!"
	sleep 5
	$bakservice=bitcoin
else

start-cli backup target mount disk-/dev/sdb1 "*umbrel*password*here*" --package-ids ${bakservice} >> /root/backup_blockchain.log 2>&1

# here loop
echo "Searching a valid disk... ";

# white
#destuuid=1f40def2-b068-4762-94bc-c3832ae0bd2f
#destlabel=usb2000
#rsyncopts="-avihH --fsync --mkpath --delete"

# wd red
#destuuid=11bd8580-ba11-4101-8aeb-509334377032
#destlabel=hdd1000
#rsyncopts="-avihH --fsync --mkpath --delete"

# ventoy black, exfat, need sudo -i
destuuid=7F09-E574
destlabel=usb1000
rsyncopts="-vrltD --delete"

#srcdir="/data/blockchain/${bakservice}" # umbrel
srcdir="/embassy-data/package-data/volumes/${bakservice}/data/main" # start9

destdev=/dev/disk/by-uuid/$destuuid
if [ -L "$destdev" ]; then
    echo "found attached $destlabel."
    destmnt=/mnt/$destlabel

    if [ ! -f "$destmnt/blockchain/blockchain.dummy" ]; then
        echo "Mounting..."
	mkdir -p $destmnt
        mount -o noatime $(readlink -f $destdev) $destmnt || exit 1
    fi
else
    # here switch to next disk
    echo "[ ! ] Don't see a valid disk. Exit."
    echo "-------------------------------------------------------------------------------"
    exit 1
fi

if [ ! -f "$destmnt/blockchain/blockchain.dummy" ]; then
	echo "Mount disk $destlabel..."
	mount -o noatime $destdev $destmnt || exit 1
fi

destdir=$destmnt/blockchain/${bakservice}
if [ -f "$destdir/../blockchain.dummy" ] && [ -w "$destdir/" ]; then
    # loop recheck, is mounted and writable
	status_rsync=1
	while [ $status_rsync -ne 0 ]; do
	    sync
		if [ ${bakservice} == "bitcoind" ]; then
                        destdir="$destmnt/blockchain/bitcoin" # simple replacement to cut trailing "d"
		elif [ ${bakservice} == "monerod" ]; then
                        destdir="$destmnt/blockchain/monero-prune" # pruned service
                else
                        destdir="$destmnt/blockchain/${bakservice}"
                fi

		echo "Backup job: ${bakservice}"
		ionice -c 2 \
		rsync \
			$rsyncopts --progress --stats \
			$srcdir/ \
			$destdir/
		status_rsync=$?
		echo "Job status: $status_rsync"
	done
	sync

	echo "Unmounting Disk $destlabel..."
	umount $destmnt
	echo "End."
	echo "-------------------------------------------------------------------------------"
else
	echo "[ ! ] Error accessing the disk $destlabel."
	echo "Try 'sudo chown -R 1000:1000 $destdir' and rerun the script. Exit."
	echo "-------------------------------------------------------------------------------"
fi
fi

return

1 Like

I did a translation…

Great idea, maybe it’s much easier. Set up Rsync job, fix mountpoint as destination e.g. deposit /mnt/usbdisk, finish.

Previously, I tinkered for Umbrel+Start9 something similar. The blockchain should be ensured rotating on different discs. Has worked well. Only a routine for the car detection of the discs I never finished (line 19-32)

2 Likes

@EXXON - Welcome to TrueNAS and the forums!

Thank you for sharing. Always good to get more information.

Years ago I also wrote up a backup method for TrueNAS, published in the old forum as below. It’s not perfect, but works perfect for me.

Feel free to borrow / steal any ideas from the procedure, or the script, (which is in the discussion section of the old forum’s Resources).

1 Like

@mratix Thanks for the ideas from your script. I just created the possibility to use all attached ext4 fromated drives for backup and cycle through them one by one every time the script gets executed. :bulb:

@ davistw Vielen Dank fĂĽr die Ăśbersetzung :slight_smile:

@Arwen Thanks for the hint, I have seen your post before but didn’t see your script. Defenetly I will copy some things around your error handling and documentation style :+1:

Maybe I put a few more things in and upload the new version soon. Happy to get your advice!

1 Like

Hello all,

i have implemented the functionality to autodetect any ext4 (or any other filesystem spedified) connected disks. You can either still specify an UUID to back up to or let the script select any ext4 disk. It will automatically cycle through the connected disks by saving the last used UUID into the $DATAFILE.

I also moved my script location to a seperate dataset called Scripts as I read that the filesystem of Truenas is not a safe location as it might be deleted on an update.

I might have overengineered the saving functionality of the last UUID but had fun while doing it and either on future amendments it might be useful or other scripts I will work on in the future.

Thanks again for your previous inputs and scripts to copy. @Arwen I copied your script documentation format :+1:
I might still implement the error codes or any bugfixes. For now this is already more then sufficient for me :slight_smile:

Feel free to use any parts of the script for yourself!

I will also edit the first post, once I have the rights to do so. Could anyone help me with this please?

#!/bin/bash
#ident  "TrueNAS backup to USB disks  0.02     2025/08/03    Matthias Geffert"
#
#   Script name:
#       rsync_backup.sh
#
#   Description:
#	  Perform the following:
#	 - Create a log file
#	 - Either backups to a specified UUID given as a parameter ./rsync_backup.sh 
#	   e.g. ./rsync_backup.sh e9624b97-f9f1-4048-af11-a91b7dc85d0c
#	   Or backups to any connected disk with the defined filesystem in configuration
#	   section and changes the disk each time the script runs
#
#   Usage:
#	   e.g. ./rsync_backup.sh
#	   e.g. ./rsync_backup.sh e9624b97-f9f1-4048-af11-a91b7dc85d0c
#
#   Author info:
#       Matthias Geffert
#       (C) Copyright Matthias Geffert
#
#   Permission and fees:
#       You have the right to run this script without any warranty. If you
#       find it useful, let me know. If it breaks something, I am not
#       responsible. If you modify this script, do not alter my credits,
#       add your own.
#
#   Notes:
#     	It is assumed that the backup will fit on the destination. Thus,
#	      checking log file after backup is helpful to detect errors.
#
#   History:
#       2025/07/31 - Matthias Geffert - First version of script
#       2025/08/04 - Matthias Geffert - Added automatic UUID selction and
#                    read/write functionality to save last used UUID
#
#   To be added:
#     - creation of exit codes
#
##############################################################################

# debug information for UUID selection
#exec 3>&1 1>"/mnt/Data/Scripts/test.log" 2>&1

##############
#Configuration
##############

DATAFILE="/mnt/Data/Scripts/rsync_backup_data.txt"
LOGFOLDER="/mnt/Data/Dokumente/backup_logs"
SOURCEFOLDERS=("/mnt/Data/Dokumente" "/mnt/Data/Bilder" "/mnt/Data/Backup" "/mnt/Data/Syncthing" "mnt/Data/Scripts")
DESTFOLDER="/mnt/backupdisk"
FILESYSTEMTYPE="ext4"

##############
#Functions
##############

#check if UUID disk is mounted
isUuidMounted() { findmnt --source UUID="$CURUUID" >/dev/null;} #UUID only

#Read data from $DATAFILE
readData() {
  SEARCHCONTENT=$1
  if test -f $DATAFILE; then
    #Read file content into arrays
    mapfile -t FILECONTENTNAMES < <( cat $DATAFILE | cut -d'=' -f1)
    mapfile -t FILECONTENTVALUES < <( cat $DATAFILE | cut -d'=' -f2)
    #Search for Parameter 
    COUNTCONTENT=${#FILECONTENTNAMES[@]}
    for (( i=0; i<$COUNTCONTENT; i++ ));
    do
      if [ "${FILECONTENTNAMES[$i]}" = "$SEARCHCONTENT" ]; then
        #Return Parameter value
        echo ${FILECONTENTVALUES[$i]}
      fi
    done
  
  else
    echo "File not found"
  fi
}

#Save data to $DATAFILE
saveData() {
  PARAMETER=$1
  PARAMETERCONTENT=$2

  if test -f $DATAFILE; then
    #Read file content into arrays
    mapfile -t FILECONTENTNAMES < <( cat $DATAFILE | cut -d'=' -f1)
    mapfile -t FILECONTENTVALUES < <( cat $DATAFILE | cut -d'=' -f2)
    #Search for parameter to write
    COUNTCONTENT=${#FILECONTENTNAMES[@]}
    for (( i=0; i<$COUNTCONTENT; i++ ));
    do
      if [ "${FILECONTENTNAMES[$i]}" = "$PARAMETER" ]; then
        FILECONTENTVALUES[$i]=$PARAMETERCONTENT
        FOUNDPARAMETER=1
      fi
    done
    #write parameter
    COUNTCONTENT=${#FILECONTENTNAMES[@]}
    WRITEARRAY=
    for (( i=0; i<$COUNTCONTENT; i++ ));
    do
      WRITEARRAY+=${FILECONTENTNAMES[$i]}"="${FILECONTENTVALUES[$i]}"\n"
    done
    #if parameter has not been found
    if [[ ! $FOUNDPARAMETER ]]; then
      WRITEARRAY+=$PARAMETER"="$PARAMETERCONTENT"\n"
    fi
  #if file has not been found
  else
    WRITEARRAY+=$PARAMETER"="$PARAMETERCONTENT"\n"
  fi
  printf ${WRITEARRAY[@]} > $DATAFILE
}

######################
#Start of backupscript
######################

#Check if UUID has been given as parameter, otherwise select one of all connected ext4 disks
if [ "$1" != "" ]; then
  CURUUID="$1"
else
  mapfile -t ALLUUIDS < <( sudo blkid -tTYPE=$FILESYSTEMTYPE -sUUID | cut -d'"' -f2)
  #mapfile -t ALLUUIDS < <( sudo blkid -tTYPE=ext4 -sUUID | cut -d'"' -f2)
  CURUUID=""
  #check if there was saved a previously backup UUID
  if test -f $DATAFILE; then
    LASTUUID=$(readData "lastUUID")
    echo "LASTUUID:"$LASTUUID
    if [ ! $LASTUUID = "" ]; then
      #loop through UUIDs to check what is the next UUID
      COUNTUUIDS=${#ALLUUIDS[@]}
      for (( i=0; i<COUNTUUIDS; i++ ));
      do
        if [ "${ALLUUIDS[$i]}" = "$LASTUUID" ]; then
          CURUUID=${ALLUUIDS[$i + 1]}
        fi
      done 
      #if the File for storage of UUID has not been found or UUID was at the end of array use the first UUID
      if [ "$CURUUID" = "File not found" ] || [ "$CURUUID" = "" ]; then
        CURUUID=${ALLUUIDS[0]}
      fi
    #if LASTUUID is empty
    else
      CURUUID=${ALLUUIDS[0]}
    fi
  #if $DATAFILE does not exist 
  else
    CURUUID=${ALLUUIDS[0]}
  fi
  #save current used UUID
  saveData "lastUUID" $CURUUID
fi

#debug info for UUID selection
echo "array0  :"${ALLUUIDS[0]}
echo "array1  :"${ALLUUIDS[1]}
echo "array2  :"${ALLUUIDS[2]}
echo "array3  :"${ALLUUIDS[3]}
echo "LASTUUID:"$LASTUUID
echo "CURUUID :"$CURUUID
#set Logfilename
LOGDATE=$(date +"%Y%m%d-%H%M%S")
LOGNAME=$LOGDATE"_"$CURUUID
#mount UUID Disk
mkdir -p /mnt/backupdisk
mount UUID=$CURUUID /mnt/backupdisk
#create logfile
mkdir -p $LOGFOLDER
LOGFILE=$LOGFOLDER"/"$LOGNAME".log"
exec 3>&1 1>"$LOGFILE" 2>&1
#Start backup
echo "Start: "$(date -R)
echo "UUID:  "$CURUUID
echo "#############################################################"
echo
#check if disk is mounted
if isUuidMounted $CURUUID;
  then
    echo   "Disk is mounted"
    echo
    echo "#############################################################"
    echo "Truenas Configuration Backup"
    echo "#############################################################"
    echo
    mkdir -p $DESTFOLDER"/truenas_config"
    rsync -rltgoDv /var/db/system/configs-*/ $DESTFOLDER"/truenas_config"
    #do backup of all specified folders
    for FOLDER in "${SOURCEFOLDERS[@]}"
    do
      echo
      echo "#############################################################"
      echo "Sourcefolder: " $FOLDER
      echo "#############################################################"
      echo
      mkdir -p $FOLDER
      rsync -rltgoDv $FOLDER $DESTFOLDER
    done
  else
    echo   "Disk is not mounted"
fi
#unmount disk
umount /mnt/backupdisk

echo
echo "#############################################################"
echo "End: "$(date -R)
echo "#############################################################"

exit 0

1 Like