Skip to content

Instantly share code, notes, and snippets.

@jivanpal
Last active May 28, 2026 11:20
Show Gist options
  • Select an option

  • Save jivanpal/2485c780df4a3d7229a7b98fbc70e35e to your computer and use it in GitHub Desktop.

Select an option

Save jivanpal/2485c780df4a3d7229a7b98fbc70e35e to your computer and use it in GitHub Desktop.
Backup script (using Restic) and SystemD units.
#!/bin/bash
# Lives at /usr/local/bin/backup
if [ $EUID -ne 0 ]; then
>&2 echo "ERROR: This script must be run as root."
exit 1
fi
if [ $# -eq 0 ]; then
>&2 echo "ERROR: Missing required args: $0 <backup-set name> [restic args]"
exit 1
fi
backup_set="$1"
shift
## Set envvars
backend_nadine="sftp:jivan@restic.jivan.dev:/mnt/restic"
backend_hetzner="sftp:restic-hetzner.jivan.dev:/home/restic"
case "$backup_set" in
media-data)
repo_name="media-data"
;;
*)
repo_name="default"
;;
esac
case "$repo_name" in
default)
backend_base_path="$backend_nadine"
#backend_base_path="$backend_hetzner"
;;
media-data)
backend_base_path="$backend_nadine"
;;
*)
>&2 echo "ERROR: Got invalid repo name: $repo_name"
exit 1
esac
export HOME=/root # For Restic cache
export RESTIC_REPOSITORY="$backend_base_path/repos/$repo_name"
export RESTIC_PASSWORD_FILE=/etc/restic/password
## Define backup sets
docker_service=false
docker_service_update=true
case "$backup_set" in
*--no-update)
docker_service_update=false
backup_set="${backup_set%--no-update}"
;;
esac
case "$backup_set" in
dawarich)
docker_service=true
docker_project_dir="/var/www/email.pal.maps"
backup_dirs="$docker_project_dir"
;;
immich)
docker_service=true
docker_project_dir="/var/www/email.pal.photos"
backup_dirs="$docker_project_dir"
;;
jellyfin)
docker_service=true
docker_project_dir="/var/www/com.jivanpal.media"
backup_dirs="/var/www/com.jivanpal.media --exclude /var/www/com.jivanpal.media/cache"
;;
media-data)
backup_dirs="/mnt/media-data --exclude /mnt/media-data/lost+found"
;;
palworld)
backup_dirs="/var/www/email.pal.world --exclude-file /etc/restic/exclude-palworld"
;;
transmission)
docker_service=true
docker_project_dir="/etc/transmission-daemon"
backup_dirs="$docker_project_dir /var/lib/transmission-daemon"
;;
vaultwarden)
docker_service=true
docker_project_dir="/var/www/email.pal.vault"
backup_dirs="$docker_project_dir"
;;
*)
>&2 echo "ERROR: Unknown backup-set \"$backup_set\""
exit 1
esac
## Perform backup
backup_tags="$backup_set"
if $docker_service; then
# We assume that the first line in the Docker service's `.env` file
# specifies the app's / primary container's image version.
docker_service_version="$(head -1 "$docker_project_dir/.env" | cut -d= -f2)"
backup_tags="$backup_tags,$docker_service_version"
if $docker_service_update; then (
set -x
docker compose --project-directory "$docker_project_dir" pull
docker compose --project-directory "$docker_project_dir" down
); fi
fi
hostname="$(hostname -f)"
echo "+ RESTIC_REPOSITORY=$RESTIC_REPOSITORY"
(
set -x
restic -vv \
backup $backup_dirs \
-H "$hostname" \
--tag "$backup_tags" \
"$@" |
grep --line-buffered -Ev '^unchanged '
)
if $docker_service && $docker_service_update; then (
set -x
docker compose --project-directory "$docker_project_dir" up -d
); fi
# Lives at /etc/systemd/system/backup@.service
[Unit]
Description=Backup data to Restic repository
[Service]
Type=exec
ExecStart=/usr/local/bin/backup %i
# Lives at /etc/systemd/system/backup@.timer
[Unit]
Description=Backup data to Restic repository
[Timer]
OnCalendar=04:00
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment