If you have started self hosting applications, you soon realize “what happens to all of my data if my house (or datacenter) disappeared?”. Having a good backup strategy is an important step in any self-hosted infrastructure.
I’m using this process for backing up my Immich and Syncthing apps (see what I use for more details)
but it can be broadly applied to anything you backup as long as it has one or more file directories containing the data to backup.
3-2-1 Backup Strategy
There are a lot of good articles online about the 3-2-1
backup strategy (such as this one), so I won’t repeat them. I’d say the TLDR is:
- have 3 copies of your data - the original data and two backup copies
- have one backup copy onsite for quick recovery of a drive or other hardware failure
- have one backup copy offsite for disaster recovery if primary location disappears (fire, theft, etc)
Why Restic
There are a lot of good backup tools out there and Restic met the criteria I was looking for.
- free and open-source
- supports different backend storage types like
SFTP
andS3
- easy to implement encryption
- it has been around long enough to trust that issues have been worked out and it will likely continue to be supported
Backup Wrapper Script
I have a few different apps to backup, so I created a central re-usable backup script. This script expects certain environment variables to be set before it is called so I can keep those separate for each app.
Contents of /opt/restic/backup.sh
- you should be able to use this without any changes:
#!/bin/bash
# Full path to the restic executable
RESTIC_BIN="/usr/local/bin/restic"
# Function to log messages
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S'): $1"
}
# Do this inside a block that requires the lock to prevent multiple backups from running at once.
(
# Confirm we have a lock.
flock -n 99 || { echo "ERROR - Another instance of the script is already running. Exiting."; exit 1; }
# Backup to both our local and remore repos (if defined).
for REPO in $LOCAL_REPO $REMOTE_REPO; do
# You need to run restic init the first time to create the repository. Use the cat command
# to check if repo exists as recomended by the scripting guide: https://restic.readthedocs.io/en/latest/075_scripting.html
"$RESTIC_BIN" -r $REPO cat config > /dev/null
CAT_EXIT_CODE=$?
# Exit code 10 is returned if everything works but repo doesn't exist.
# NOTE: This requires restic version 0.17.0 or above.
if [ $CAT_EXIT_CODE -eq 10 ]; then
log "Restic repo ${REPO} appears to be empty - initializing it."
"$RESTIC_BIN" -r $REPO init
INIT_EXIT_CODE=$?
if [ $INIT_EXIT_CODE -ne 0 ]; then
log "ERROR - Restic init command ${REPO} failed with exit code ${INIT_EXIT_CODE}."
exit 1
fi
elif [ $CAT_EXIT_CODE -ne 0 ]; then
log "ERROR - Restic cat command for ${REPO} failed with exit code ${CAT_EXIT_CODE}."
exit 1
fi
# Run the backup
log "********** Starting Restic backup to ${REPO} **********"
"$RESTIC_BIN" -r $REPO backup $BACKUP_PATHS --limit-upload 5000 -v --tag "cron-daily"
BACKUP_EXIT_CODE=$?
if [ $BACKUP_EXIT_CODE -ne 0 ]; then
log "ERROR - Restic backup command to ${REPO} failed with exit code ${BACKUP_EXIT_CODE}."
exit 1
fi
log "Backup to ${REPO} completed successfully."
# Forget old snapshots and prune the repository
log "**** Starting forget and prune to ${REPO} ****"
"$RESTIC_BIN" -r $REPO forget $FORGET_POLICY --prune
FORGET_EXIT_CODE=$?
if [ $FORGET_EXIT_CODE -ne 0 ]; then
log "ERROR - Restic forget/prune command failed with exit code $FORGET_EXIT_CODE."
exit 1
fi
log "Prune of ${REPO} completed successfully."
done
log "********** Restic backup finished **********"
) 99>"$LOCK_FILE"
exit 0
Environment Files
Then I setup two environment vars files for each app - one which is stored as plain text in source control and one is encrypted to protect the keys. Having most of the settings in plain-text helps to easily view them in Git and track diffs over time.
Contents of /opt/immich/backup-settings.sh
(replace any token wrapped with <
and >
for your environment):
#!/bin/bash
# Backup URLs and Access Key
export AWS_ACCESS_KEY_ID="<YOUR_KEY_ID>"
export LOCAL_REPO="sftp:<LOCAL_SERVER>:<BACKUP_DIR>"
export REMOTE_REPO="s3:s3.<REGION>.backblazeb2.com/<YOUR_BUCKET>/immich"
# Directories to back up
export BACKUP_PATHS="/<IMMICH_DIRECTORY>/library/backups /<IMMICH_DIRECTORY>/library/library /<IMMICH_DIRECTORY>/library/profile /<IMMICH_DIRECTORY>/library/upload"
# Lock file to prevent multiple instances
export LOCK_FILE="/var/lock/backup-immich.lock"
# Retention policy for 'forget' command
export FORGET_POLICY="--keep-monthly 12 --keep-yearly 5"
Contents of /opt/immich/backup-keys.sh
(replace any token wrapped with <
and >
for your environment):
#!/bin/bash
export AWS_SECRET_ACCESS_KEY="<YOUR_SECRET_KEY>"
export RESTIC_PASSWORD="<YOUR_ENCRYPTION_PASSWORD>"
Running with CRON
This is run daily by dropping a file in /etc/cron.daily
.
Contents of /etc/cron.daily/backup-immich
:
#!/bin/bash
APP=immich
exec >> "/var/log/backup-${APP}.log" 2>&1
# NOTE: This next line is Immich specific but provides a good idea of how to do something special for a single app
# before the regular backup runs. This uses "podmad exec" but you can easily replace with "docker exec" if
# you use Docker.
#
# Backup database right before we run the backup script so our DB and filesystem are as close as possible. Don't
# compress DB backup so restic can handle deduplicating as much of content as possible.
podman exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > /<IMMICH_DIRECTORY>/library/backups/immich-database.sql
# Perform a local and offsite backup via restic backup script.
source /opt/${APP}/backup-settings.sh && source /opt/${APP}/backup-keys.sh && /opt/restic/backup.sh
Note that doing a manual DB backup means you can disable the scheduled Immich backups. This is similar to how Immich recommends running backups in their Borg backup docs.
Troubleshooting
For some reasons it always takes me some time to get cron jobs to run smoothly. It is important to monitor the output
of the logs for at least a few days and make sure everything is working. Ideally you should also do a restore test to a test
environment a regular basis to confirm the backup process is working.
Common problems I encounter are:
- the path and environment vars in cron jobs are different than your regular shell
cron
is running asroot
instead of your user so SSH keys and other config are different- file permissions on your script files can keep them from executing
- typos are not easy to notice
Adding Other Apps
You can basically follow the same approach for any other apps. Create the two environment files and a cron
file and you easily expand to backup other items.