How to properly schedule?

Hi there,

While setting up kopia cli for my use-case I came to the point where I am wondering how to properly schedule the snapshots for my repositories?

I want to backup the data of my docker containers which is at the volumes folder of the host where I run all my containers via docker-compose. I setup one repository per container and set the retention policy, everything configured in ansible. But how to do the scheduling part now?

Is it cron? Is it installing copia as server e.g. as systemd service? If I use cron, do I not only have to run the snapshot command but also switch repositories inbetween? I strolled the documentation but I have no clear answer till now, thus I liked to ask.

Thanks in advance,
Mango

There are people way more informed that might answer but I’ve run systemd timer script that works well. For each repo that is to be backed up the script will connect to the relevant repo, set up logging, run snapshot and any other maintenacne task you need then disconnect and connect to the next repo and so on. Passwords are loaded as an env variable.
Various repos have different backup schedules and I use the sync-to command to create a backup of the backup repos.

$ cat kopia_snapshot_NAS-1-2.timer
[Unit]
Description=Run kopia daily snapshot of NAS-1-2

[Timer]
OnCalendar=Mon,Tue,Thu..Sat *-*-* 4:00:00
Persistent=false
Unit=kopia_snapshot_@NAS-1-2.service
WakeSystem=true

[Install]
WantedBy=timers.target

$ cat kopia_snapshot_@.service
[Unit]
Description=kopia snapshot %i service

Wants=network-online.target
After=network-online.target
ConditionACPower=true

[Service]
Environment="HOME=/root"
Type=oneshot
Nice=19
CPUSchedulingPolicy=batch
IOSchedulingClass=best-effort
IOSchedulingPriority=7
CPUWeight=60
CPUQuota=90%
IOWeight=60
MemorySwapMax=0
Restart=no
LogRateLimitIntervalSec=0
StandardOutput=journal+console
StandardError=journal+console
# %i = NAS-1-2-3-4, NAS-3-4, NAS-1-2
ExecStart=/usr/local/bin/kopia/kopia_snapshot.sh %i

Hey ted, thanks for your answer. I will try it out as soon as I have some more time to work on the project :slight_smile:

could you share “/usr/local/bin/kopia/kopia_snapshot.sh” also? thanks!

“/usr/local/bin/kopia/kopia_snapshot.sh”
has a lot of code that is probably unhelpful to answering the question and specific to my use case. Also, might raise other random questions that are off topic.
It contains

  1. log initialisation
  2. disk mounting procedure for the source and repo drives
  3. preexec and postexec routines
  4. Repo connection commands
  5. Repo status, info, maintenance info output
  6. Snapshot creation

On schedule it runs a snapshot, sync-to and repo verify.

If you have a more specific question it might be better to start another post.

@ted thanks, this helps already to understand the basic workflow.
I will look up your advanced usage of systemd-unit-naming … and try to adjust it to my needs.
thanks

For clarity, my various repos have these various timers set up in directory:

/etc/systemd/system

kopia_host1_snapshot_NAS-1-2-3-4.timer
kopia_host1_snapshot_NAS-1-2.timer
kopia_host1_snapshot_NAS-3-4.timer

kopia_host1_snapshot_@.service

If you are not so familiar with systemd the repo names are substituted as args into the service file, so that to run the respective service timers directly (immediately) you can also use (in generic form):

systemctl start kopia_host1_snapshot_@<SNAPSHOT_GROUP>.service
systemctl stop kopia_host1_snapshot_@<SNAPSHOT_GROUP>.service
systemctl status kopia_host1_snapshot_@<SNAPSHOT_GROUP>.service

or by specific example

systemctl start kopia_host1_snapshot_@NAS-1-2.service
systemctl start kopia_host1_snapshot_@NAS-1-2-3-4.service
systemctl start kopia_host1_snapshot_@NAS-3-4.service

Given the systemd timer/service is calling my snapshot creation script I can run it directly also using (specific example):

nice -n 19 sudo bash ./kopia_snapshot.sh NAS-1-2-3-4

Description=kopia snapshot %i service
From:
https://www.freedesktop.org/software/systemd/man/systemd.service.html

%I -It is possible for systemd services to take a single argument via the
“service@argument.service” syntax. Such services are called “instantiated”
services, while the unit definition without the argument parameter is called
a “template”. An example could be a dhcpcd@.service service template which
takes a network interface as a parameter to form an instantiated service. In
the service file, this parameter or “instance name” can be accessed with %-specifiers.
See systemd.unit(5) for details.
%i lower case i preserves the escapes

Using linux, my computer wakes from suspend at the timer allocated time, does the backup then goes back into suspend, unless I start using it for other purposes or already using it. So backups happen as long as it is in suspend.

1 Like

I’ve managed to clean up my /usr/local/bin/kopia/kopia_snapshot.sh to give you here in case it helps. Bear in mind it may contain some areas in ‘note form’ and may not present the best way to do something or be useful for your use case. It also contain hidden code from other functions not given.

#!/bin/bash

### Kopia backup script template

### https://github.com/kopia/kopia/releases
### https://kopia.io/docs/reference/command-line/common/snapshot-verify/

### Run this script preferably via systemd service timer or otherwise directly 
### from the shell terminal

### nice -n 19 sudo bash ./kopia_snapshot.sh <SNAPSHOT_GROUP>
### nice -n 19 sudo bash ./kopia_snapshot.sh NAS-1-2
### nice -n 19 sudo bash ./kopia_snapshot.sh NAS-1-2-3-4

function usage(){
    local usagetext
    IFS='' read -r -d '' usagetext <<-'EOFMARKER' || true
	Usage:
		nice -n 19 sudo bash ./kopia_snapshot.sh <SNAPSHOT_GROUP>
		or
		systemctl start kopia_host1_snapshot_@<SNAPSHOT_GROUP>.service
		systemctl stop kopia_host1_snapshot_@<SNAPSHOT_GROUP>.service
		systemctl status kopia_host1_snapshot_@<SNAPSHOT_GROUP>.service

	Arg: (1 only)
		SNAPSHOT_GROUP	The snapshot selection to run that defines the source and 
						destination of the data.
		where SNAPSHOT_GROUP can be one of:

		NAS-1-2 		 host1 snapshot of NAS-1, NAS-2 to backup hdd
		NAS-1-2-3-4		 host1 snapshot of NAS-1, NAS-2, NAS-3, NAS-4 to backup hdd
		NAS-3-4			 host1 snapshot of NAS-3, NAS-4 to backup hdd
		
	Example:
		Run snapshot for host1 NAS-1-2 hdds:
		$ nice -n 19 sudo bash ./kopia_snapshot.sh NAS-1-2-3-4
	EOFMARKER
    printf "%s" "${usagetext}"
}

function kopia_snapshot(){
    text="Starting fn kopia_snapshot"
    fn_header ${FUNCNAME[0]} ${LINENO} 'hash' "$text"
    
    #
    # Create backups
    #
    # Security considerations
    #--------------------------
	export KOPIA_CACHE_DIR="/home/user01/.cache/kopia/"
    
    # Log Kopia version
    logit "Kopia version: |$(kopia --version)|"

    DATE=$(date +%Y-%m-%d_%H-%M-%S.%3N)
    notify info "Starting SNAPSHOT |${DATE}|${SNAPSHOT_GROUP}"
    
    #--------------------------------------------------------------------------
    # The backup partitions to be used are mounted here
	case "${HOSTNAME}" in
		host1)
			case "${SNAPSHOT_GROUP}" in
				'NAS-1-2')
					selective_mount 1 2 5
					MOUNTPOINTS=( "/mnt/hdd5" )
					REPONAME=( "root" "home" "NAS-1" "NAS-2" )
					SOURCE=( "/" "/home/" "/mnt/hdd1/" "/mnt/hdd2/" )
					;;
				'NAS-3-4')
					selective_mount 3 4 5
					MOUNTPOINTS=( "/mnt/hdd5" )
					REPONAME=( "NAS-3" "NAS-4" )
					SOURCE=( "/mnt/hdd3/" "/mnt/hdd4/" )
					;;
				'NAS-1-2-3-4')
					selective_mount 1 2 3 4 5
					MOUNTPOINTS=( "/mnt/hdd5" )
					REPONAME=( "root" "home" "NAS-1" "NAS-2" "NAS-3" "NAS-4" )
					SOURCE=( "/" "/home/" "/mnt/hdd1/" "/mnt/hdd2/" "/mnt/hdd3/" "/mnt/hdd4/" )
					;;
				*)
					logit "ERROR - No SNAPSHOT_GROUP found. Looking for |${SNAPSHOT_GROUP}|"
					exit
					;;
			esac
			;;
		*)
			logit "ERROR - No HOST found. Looking for |${SNAPSHOT_GROUP}|"
			exit
			;;
		esac
    #--------------------------------------------------------------------------
    
    for MOUNTPOINT in "${MOUNTPOINTS[@]}"; do # Handle multiple backup copies on various mount points
		echo
		logit "|${BASH_SOURCE}|$0| Fn: ${FUNCNAME[0]} | FOR LOOP MOUNT POINT Processing: |$MOUNTPOINT|"

		BASEDIR="${MOUNTPOINT}/${HOSTNAME}_kopia"
		# DO NOT CREATE AUTOMATICALLY (false) BEST DONE MANUALLY AS NEEDED. RATHER USE AS A TEST FOR MOUNTED BELOW
		if [ ! -d "${BASEDIR}" ] && false; then 
			logit "No directory ${BASEDIR}. Creating now..."
			install -dvm 777 -o root -g root "${BASEDIR}"
			echo -e "Prevent writes to the directory by setting immutable bit i.  View bits by running $ lsattr"
			/usr/bin/chattr +i "${BASEDIR}"
		fi
		

		check_permissions 'root'
		mountpoint "$MOUNTPOINT"
		[ -d "${BASEDIR}" ] && echo $?
		if mountpoint -q "$MOUNTPOINT" && [ -d "${BASEDIR}" ]; then
			logit "Mountpoint |$MOUNTPOINT| exists"
		else
			logit "Mountpoint |$MOUNTPOINT| DNE. Continuing to the next"
			continue # If repo DNE then skip to the next
		fi


		# 2 = lockonly for nas-3-4
		# where lock is a file that prevents suspend of computer
		if [ "${SNAPSHOT_GROUP}" = "NAS-3-4" ] || [ "${SNAPSHOT_GROUP}" = "testing" ]; then
			logit "preexec being locked but not run..."
			preexec "${BASEDIR}" lockonly "${SNAPSHOT_GROUP}" "$SCRIPTS_DIR2"
		else
			logit "preexec running..."
			preexec "${BASEDIR}" - "${SNAPSHOT_GROUP}" "$SCRIPTS_DIR2" # "$MOUNTPOINT"
		fi
		
		logit "preexec finished"

		for ((i=0; i<${#REPONAME[@]}; i++)); do
			echo
			logit "###############################################################################################################"
			logit "|$0| Fn: ${FUNCNAME[0]}() FOR LOOP REPO Processing |i=$i|${SOURCE[i]}|${REPONAME[i]}|"
			DATE=$(date +%Y-%m-%d_%H-%M-%S)

			# Location of the Kopia repository, create if DNE
			REPO="${BASEDIR}/${REPONAME[i]}"
			LOGDIR="${BASEDIR}/${REPONAME[i]}_log"
			

			CREATENAME="${HOSTNAME}_${REPONAME[i]}_CREATE_${DATE}"
			echo -e "Check REPO:|${REPO}|LOGDIR:|${LOGDIR}|CACHE:|CACHEDIR|"

			# Log file used for function log output - Need to initialise
			FILEPATHOUT="" 
			init_logfile "${LOGDIR}" "${CREATENAME}.log" "root"
			LOGPATH="${FILEPATHOUT}"
			echo -e "Check FILEPATHOUT, LOGPATH |<${FILEPATHOUT}> <${LOGPATH}>|"

			# New content generated function log file
			init_logfile "${LOGDIR}" "${CREATENAME}_content.log" "root"
			CONTENTLOGPATH="${FILEPATHOUT}"
			echo -e "Check FILEPATHOUT:<${FILEPATHOUT}>"
			echo -e "Check CONTENTLOGPATH:<${CONTENTLOGPATH}>"

			echo
			notify info "Starting back-up source |${SOURCE[i]}| to repo |${REPO}|$DATE|"
			logit "| $SCRIPTS_DIR2 | ${SOURCE[i]} | ${REPONAME[i]} | $REPO |"

			echo
			if [ ! -d "${REPO}" ]; then
				logit "No repo directory for ${REPO}. Creating now..."
				install -dvm 700 -o root -g root "${REPO}"
				kopia repository create filesystem \
					--path "${REPO}" \
					-p "$KOPIA_PASSWORD"

				logit "NOTE: To validate that your provider is compatible with Kopia, please run: $ kopia repository validate-provider"
				kopia repository validate-provider
			else
				logit "Repo <${REPO}> exists already. No creation necessary."
			fi
			logit "XXXXXXXXX Connect to the repo |${REPO}| Source: |${SOURCE[i]}|"

			kopia repository connect filesystem \
				--path "${REPO}" \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info \
				-p "$KOPIA_PASSWORD"

			if [ $? -eq 1 ]; then
				logit "Password incorrect, exiting"
				exit 1
			else
				logit "Connection SUCCESS to |${REPO}|"
			fi

			echo
			logit "Repo |${REPO}| status..."

			kopia repository status \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info

			echo
			list_policy

			echo
			logit "Repo |${REPO}| content stats raw numbers..."
			kopia content stats \
				--raw \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info

			echo
			logit "Repo |${REPO}| content stats..."
			kopia content stats \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info

			echo
			logit "Cache info path only..."
			kopia cache info \
				--path \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info
			echo
			logit "Cache info..."
			kopia cache info \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info

			echo
			logit "Show maintenance info"
			kopia maintenance info \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info

			echo
			if false; then
				logit "Snapshot ${SOURCE[i]} estimate, show all source data...|$(date +%Y-%m-%d_%H-%M-%S)|++++++++++++++++++++++++++++++++++++++++++++"
				kopia snapshot estimate \
					--upload-speed 110 \
					--log-dir "${LOGDIR}" \
					--log-file "${LOGPATH}" \
					--content-log-file "${CONTENTLOGPATH}" \
					--file-log-level info \
					--log-level info \
					"${SOURCE[i]}"
			else
				logit "kopia snapshot estimate ${SOURCE[i]}, removed to dramatically improve completion time|$(date +%Y-%m-%d_%H-%M-%S)|++++++++++++++++++++++++++++++++++++++++++++"
			fi
			echo
			DATE1=$(date)
			logit "Create snapshot |${SOURCE[i]}| and write to log |${DATE1})|+++++++++++++++++++++++"

			kopia snapshot create \
				--description "" \
				--force-hash 0 \
				--log-dir "${LOGDIR}" \
				--log-file "${LOGPATH}" \
				--content-log-file "${CONTENTLOGPATH}" \
				--file-log-level info \
				--log-level info \
				"${SOURCE[i]}" \
				-p "$KOPIA_PASSWORD"

			snapshot_exit=$?

			DATE2=$(date)
			duration=$(datediff "$DATE1" "$DATE2")

			notify info "Snapshot completed.|${REPO}| Start: ${DATE1}| End: ${DATE2}| Duration: $duration |"
			
			#--------------------------------------------------------------------------
			if [ ${snapshot_exit} -eq 0 ]; then
				notify info "Snapshot SUCCESS.|${REPO}|$(date +%Y-%m-%d_%H-%M-%S)|"
			elif [ ${snapshot_exit} -eq 1 ]; then
				notify info "Snapshot WARNINGS.|${REPO}|$(date +%Y-%m-%d_%H-%M-%S)|"
			fi

			sync_data_check

			logit "Completed backup for repo ${REPO}"
		done
		echo -e "Running postexec kopia_snapshot.sh"
		postexec "${BASEDIR}" - "$SNAPSHOT_GROUP" "$SCRIPTS_DIR2"
    done
}

function sync_data_check(){
    text=""
    fn_header ${FUNCNAME[0]} ${LINENO} 'hash' "$text"
    
    # Just to be completely paranoid
	sync
}



function check_args(){
    text=""
    fn_header ${FUNCNAME[0]} ${LINENO} 'hash' "$text"
    
    # Test no args at all or 
    if [ -z $* ]; then
		printf "No arguments provided\n"
		usage
		exit 1
    else
		printf "SUCCESS #1 - Args have been provided\n"
    fi
    if [ $# -ne 1 ]; then
		printf "Not correct number of arguments\n"
		usage
		exit 1
    else
        printf "SUCCESS - No. args = 1\n"
    fi

    ALLOWLIST='(ROOT|HOME|NAS-1|NAS-2|NAS-3|NAS-4|NAS-1-2|NAS-1-2-3-4|NAS-3-4|TESTING)'
    if [[ ${1^^} =~ ^${ALLOWLIST}$ ]]; then
		printf "SUCCESS - Arg #1 |%s| is in list %s\n" ${1^^} ${ALLOWLIST}
    else
		printf "ERROR - Arg #1 |%s| is not in list %s\n" ${1^^} ${ALLOWLIST}
		usage
		exit 1
    fi
}


function main(){
    text="$0 Main start"
    fn_header ${FUNCNAME[0]} ${LINENO} 'hash' "$text"
    
    check_args "$@"
        
    check_permissions 'root'

    # NAS-1-2|NAS-1-2-3-4|NAS-3-4|etc
    SNAPSHOT_GROUP="$1"
    logit "Snapshot group parameter arg: |$SNAPSHOT_GROUP|"

    SCRIPTS_DIR=.
    SCRIPTS_DIR2="$SCRIPTS_DIR"/scripts

	if [ -z "$KOPIA_PASSWORD" ]; then
		export KOPIA_PASSWORD=<Encrypted file extraction goes here>
	fi

	#-------------------------------------------------------
	# For manual running to correct missed days.
	RUN_SYNC_TO_ONLY=false
	#-------------------------------------------------------
	if $RUN_SYNC_TO_ONLY; then
		for HOSTNAME_IN in 'host1' 'host2' 'host3' 'host4'; do
			# Mount the backup hdds
			selective_mount 5 11 12
			nice -n 19 /usr/bin/bash ./kopia_sync-to.sh "${HOSTNAME_IN}"
		done
	else
		if true; then
			kopia_snapshot
		fi

		if [ "$HOSTNAME" = host1 ]; then
			## Run verify only on Sundays
			DOW=$(date +%u)
			#---------------------------------------
			# veryify.sh schedule
			#---------------------------------------
			case "$DOW" in
				7)  # Run verify on Sun after NAS-1-2-3-4 backup
					time nice -n 19 /usr/bin/bash ./kopia_verify.sh "${SNAPSHOT_GROUP}"
					;;
				*)
					echo -e "Not running <kopia_verify.sh> today | DOW: |<${DOW}>|"
				;;
			esac

			#---------------------------------------
			# sync-to schedule
			#---------------------------------------
			case "$DOW" in
				3|7)  # Run verify then sync-to on Sun/Wed after NAS-1-2-3-4 backup
					echo
					selective_mount 11 12
					for HOSTNAME_IN in 'host2' 'host4' 'host1' 'host3'; do
						nice -n 19 /usr/bin/bash ./kopia_sync-to.sh "${HOSTNAME_IN}"
					done
					;;
				*)
					echo -e "Not running <kopia_sync-to.sh> today | DOW: |<${DOW}>|"
				;;
			esac
		else
			# Other PCs verify always
			time nice -n 19 /usr/bin/bash ./kopia_verify.sh "${SNAPSHOT_GROUP}"
		fi
	fi
    logit "COMPLETED"

	# Dismount the backup hdds
	selective_dismount 5 11 12
}


main "$@"
3 Likes

great, thanks! I just came back here to grep your files and start playing with it. Now I find your new replies with detailled explanations, wonderful. Will check asap.

I think of two scenarios currently:

  • customers: backup customer servers … where kopia would run as root user, via timer etc
  • my systems: not yet sure if to use it with my non-root user and/or root … learning

I still have to read your postings, thanks again

I spent alot of time trying both options because when mounting I wanted to do that as a user but had issues trying to run other functions or commands as root. In the end I went back to make it all run as root. In some instances I had to try to get some commands to run as user. In those cases I had to set the environment variables to get them to work. The execution of commands between user and root within a script is something I still have issues with and causes me many problems initially and have not yet found a good concise resource to understanding how to work it properly with the varous permissions and env variables.

In all cases though one has to automate the input of passwords so the timer can run without user input. I created encrypted files to do this loading them into env variables. Perhaps not the best way but a way I understood and got working for my system.

Thanks.

I go for connecting with the token, inside a properly protected (permissions) bash-script. Seems to work so far, improving things step by step now.