Restic backup script

Posted on Aug 25, 2023

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.