Restic backup script
I’ve just setup a backup script for a VPS, but this time I’ve based this on a decent (and stolen) script. We are also using healthchecks.io to get alerted if the backup isn’t run.
First up, I’ve based my backup on this simple but nice
script. But instead of using cron for running it, I’m
using a systemd-timer
to trigger a systemd-service
. The only
downside to this script is that you can only backup one directory at a
time.
I’m backing up to a S3 compatible repository so we need to set a few environment variables for this to work.
restic-backup.sh
#
Pasting the script below in case it ever goes away. Thank you very much, paolobasso99! I’ve only removed some comments and the line that sources the profile environment, as we inject variables using systemd instead.
#!/bin/bash
# Usage:
# ./backup.sh [REPOSITORY] [FOLDER_PATH] [HEALTHCHECKS_ID]
# [REPOSITORY] is the the restic repository
# [FOLDER_PATH] is the folder path to backup
# [HEALTHCHECKS_ID] (optional) healthchecks.io ID
# Example:
# ./backup.sh /my-restic-repo /path/to/backup \
# 0000000-healthchecks.io-id-000000
# To exclude a folder you can place an empty file called ".resticignore"
# inside it.
# Define functions
helpFunction() {
echo ""
echo "Usage: $0 [REPOSITORY] [FOLDER_PATH]"
echo "\t[REPOSITORY] is the path of the repository"
echo "\t[FOLDER_PATH] is the folder path to backup"
echo "\t[HEALTHCHECKS_ID] (optional) healthchecks.io ID"
exit 1 # Exit script after printing help
}
checkInputs() {
# Print helpFunction in case parameters are empty
if [ -z "${REPOSITORY}" ] || [ -z "${FOLDER_PATH}" ]; then
echo "Some or all of the parameters are empty. Exiting."
helpFunction
fi
}
lockFile() {
exec 99>"${LOCKFILE}"
flock -n 99
RC=$?
if [ "$RC" != 0 ]; then
echo "This restic ${REPOSITORY} backup of ${FOLDER_PATH} is already running. Exiting."
exit
fi
}
startHealthCheck() {
if [ -n "${HEALTHCHECKS_ID}" ]; then
echo "Sending start ping to healthchecks.io at $(datestring)"
curl -m 1 -fsS --retry 5 https://hc-ping.com/${HEALTHCHECKS_ID}/start
echo ""
fi
}
stopHealthCheck() {
if [ -n "${HEALTHCHECKS_ID}" ]; then
echo "Sending stop ping to healthchecks.io at $(datestring)"
curl -m 1 -fsS --retry 5 https://hc-ping.com/${HEALTHCHECKS_ID}
echo ""
fi
}
failHealthCheck() {
if [ -n "${HEALTHCHECKS_ID}" ]; then
echo "Sending fail ping to healthchecks.io at $(datestring)"
curl -m 1 -fsS --retry 5 https://hc-ping.com/${HEALTHCHECKS_ID}/fail
echo ""
fi
}
runBackup() {
restic -r $REPOSITORY backup --verbose --exclude-if-present .resticignore $FOLDER_PATH
if [ $(echo $?) -eq 1 ]; then
echo "Fatal error detected!"
failHealthCheck | tee -a $LOG_FILE
echo "Cleaning up lock file and exiting."
rm -f ${LOCKFILE} | tee -a $LOG_FILE
exit 1
fi
}
pruneOld() {
echo "Prune old backups at $(datestring)"
restic -r $REPOSITORY forget --verbose --prune --keep-last 10 --keep-hourly 1 --keep-daily 10 --keep-weekly 5 --keep-monthly 14 --keep-yearly 100
echo "Check repository at $(datestring)"
restic -r $REPOSITORY check --verbose
}
datestring() {
date +%Y-%m-%d\ %H:%M:%S
}
SCRIPTNAME=$(basename $0)
REPOSITORY="$1"
FOLDER_PATH="$2"
HEALTHCHECKS_ID="$3"
# Create log file
START_TIMESTAMP=$(date +%s)
LOG_FILE="${MY_RESTIC_LOGS_PATH}/$(date +%Y-%m-%d-%H-%M-%S).log"
touch $LOG_FILE
echo "restic backup script started at $(datestring)" | tee -a $LOG_FILE
echo "REPOSITORY=${REPOSITORY}" | tee -a $LOG_FILE
echo "FOLDER_PATH=${FOLDER_PATH}" | tee -a $LOG_FILE
echo "HEALTHCHECKS_ID=${HEALTHCHECKS_ID}" | tee -a $LOG_FILE
# Start helthcheck
startHealthCheck | tee -a $LOG_FILE
# Check inputs
checkInputs | tee -a $LOG_FILE
# Create lockfile
LOCKFILE="/tmp/my_restic_backup.lock"
lockFile | tee -a $LOG_FILE
# Run the backup
runBackup | tee -a $LOG_FILE
# Prune old backups
pruneOld | tee -a $LOG_FILE
# clean up lockfile
rm -f ${LOCKFILE} | tee -a $LOG_FILE
# Stop helthcheck
stopHealthCheck | tee -a $LOG_FILE
delta=$(date -d@$(($(date +%s) - $START_TIMESTAMP)) -u +%H:%M:%S)
echo "restic backup script finished at $(datestring) in ${delta}" | tee -a $LOG_FILE
restic-backup.timer
#
I’ve saved this to /etc/systemd/system/restic-backup.timer
.
Specifying unit is redudant but I kept it due to reasons.
[Unit]
Description=Run daily restic backup
[Timer]
Unit=restic-backup.service
OnCalendar=*-*-* 4:15:00
[Install]
WantedBy=timers.target
restic-backup.service
#
I’m using restic to run this backup, so some restic specific settings
below. I’ve saved my backup.sh
to /srv/backup/backup.sh
, update
below to your setup. Remember to change /PATH/TO/BACKUP
which is
the folder you want to backup.
I’m also injecting my necessary environment variables from
/root/.backup_env
, and passing them to the child process with
PassEnvironment
. To make restic a bit more efficient I’m also
passing a cache directory (which will be /var/cache/restic
).
[Unit]
Description=Run restic backup
After=network.target
[Service]
Type=oneshot
WorkingDirectory=/srv/backup
CacheDirectory=restic
EnvironmentFile=/root/.backup_env
PassEnvironment=RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY RESTIC_PASSWORD HEALTHCHECK_ID
ExecStart=/srv/backup/backup.sh $RESTIC_REPOSITORY /PATH/TO/BACKUP/ $HEALTHCHECK_ID
[Install]
WantedBy=multi-user.target
Environment file #
Below is enough to make restic backup to a S3 compatible repository.
I’ve saved this to /root/.backup_env
, if you change it, remember to
change the restic-backup.service
as well!
RESTIC_REPOSITORY=s3:https://s3.example.com/restic-repository
RESTIC_CACHE_DIR="/var/cache/restic"
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
RESTIC_PASSWORD=""
HEALTHCHECK_ID=""
Testing and enabling #
systemctl daemon-reload
# Test if the service actually runs
systemctl start restic-backup.service
journalctl -u restic-backup.service
# If everything is OK, enable the timer
systemctl enable --now restic-backup.timer
And there you have it! Automatic backups, with automatic notification if the backup doesn’t run as expected.