Last active
July 17, 2026 11:14
-
-
Save Alistair1231/b95f7c4a0b2b0e8474297b43675088db to your computer and use it in GitHub Desktop.
restic backup script for docker bind mounts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Installs or updates the restic backup setup into /opt/restic and wires up | |
| # the systemd service + timer. Run as root from a checkout of this repo: | |
| # sudo ./install.sh | |
| # | |
| # Safe to re-run to pick up updates: restic.sh and the systemd units are | |
| # always refreshed, but restic.env and restic.exclude (the per-machine | |
| # config) are only ever created if missing, never overwritten. | |
| set -euo pipefail | |
| if [ "$(id -u)" -ne 0 ]; then | |
| echo "install.sh must be run as root (sudo ./install.sh)" >&2 | |
| exit 1 | |
| fi | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| INSTALL_DIR=/opt/restic | |
| UNIT_DIR=/etc/systemd/system | |
| mkdir -p "$INSTALL_DIR" | |
| install -m 0700 "$SCRIPT_DIR/restic.sh" "$INSTALL_DIR/restic.sh" | |
| new_config=0 | |
| if [ ! -f "$INSTALL_DIR/restic.env" ]; then | |
| install -m 0600 "$SCRIPT_DIR/restic.env.example" "$INSTALL_DIR/restic.env" | |
| new_config=1 | |
| fi | |
| if [ ! -f "$INSTALL_DIR/restic.exclude" ]; then | |
| install -m 0644 "$SCRIPT_DIR/restic.exclude.example" "$INSTALL_DIR/restic.exclude" | |
| fi | |
| install -m 0644 "$SCRIPT_DIR/restic-backup.service" "$UNIT_DIR/restic-backup.service" | |
| install -m 0644 "$SCRIPT_DIR/restic-backup.timer" "$UNIT_DIR/restic-backup.timer" | |
| systemctl daemon-reload | |
| systemctl enable --now restic-backup.timer | |
| echo "Installed restic.sh, restic-backup.service, restic-backup.timer." | |
| if [ "$new_config" -eq 1 ]; then | |
| echo | |
| echo "New machine: $INSTALL_DIR/restic.env was created from the example template." | |
| echo "Edit it (repository, backend credentials, BACKUP_TARGETS), create the" | |
| echo "password file it points at, and review $INSTALL_DIR/restic.exclude," | |
| echo "before the next scheduled run." | |
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /etc/systemd/system/restic-backup.service | |
| [Unit] | |
| Description=restic-backup | |
| [Service] | |
| Type=oneshot | |
| ExecStart=/opt/restic/restic.sh | |
| WorkingDirectory=/opt/restic |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /etc/systemd/system/restic-backup.timer | |
| [Unit] | |
| Description=restic-backup | |
| [Timer] | |
| OnCalendar=hourly | |
| Persistent=true | |
| RandomizedDelaySec=5m | |
| [Install] | |
| WantedBy=timers.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /opt/restic/restic.env | |
| # | |
| # Copy to /opt/restic/restic.env and edit for this machine. install.sh does | |
| # this automatically on first install, and never touches the file again on | |
| # later updates. | |
| export RESTIC_PASSWORD_FILE="/opt/restic/.restic-password" | |
| # Create the password file once per machine: | |
| # printf '%s' 'your-password' > /opt/restic/.restic-password && chmod 600 /opt/restic/.restic-password | |
| # --- Backend: pick ONE block below and fill it in -------------------------- | |
| # -- local -- | |
| # export RESTIC_REPOSITORY="/path/to/repo" | |
| # -- sftp -- | |
| export RESTIC_REPOSITORY="sftp:user@host:/path/to/repo" | |
| # Only needed if not relying on ~/.ssh/config or an agent: | |
| export SSH_KEY="/root/.ssh/id_backup" | |
| # -- rest-server -- | |
| # export RESTIC_REPOSITORY="rest:http://user:pass@host:8000/repo" | |
| # -- S3 / S3-compatible (MinIO, etc.) -- | |
| # export RESTIC_REPOSITORY="s3:host/bucket" | |
| # export AWS_ACCESS_KEY_ID="..." | |
| # export AWS_SECRET_ACCESS_KEY="..." | |
| # -- Backblaze B2 -- | |
| # export RESTIC_REPOSITORY="b2:bucket:path" | |
| # export B2_ACCOUNT_ID="..." | |
| # export B2_ACCOUNT_KEY="..." | |
| # -- rclone (any rclone remote: Google Drive, Dropbox, OneDrive, etc.) -- | |
| # export RESTIC_REPOSITORY="rclone:remote:path" | |
| # export RCLONE_CONFIG="/etc/rclone/rclone.conf" | |
| # --- Paths to back up, space-separated -------------------------------------- | |
| export BACKUP_TARGETS="$HOME /etc" | |
| # --- Retention policy: each is independent, comment out any you don't ------ | |
| # --- want enforced. If all four are commented out, forget/prune is -------- | |
| # --- skipped entirely (check still runs). ---------------------------------- | |
| export KEEP_LAST=24 | |
| export KEEP_DAILY=7 | |
| export KEEP_WEEKLY=4 | |
| export KEEP_MONTHLY=2 | |
| # --- Optional overrides (defaults shown) ------------------------------------- | |
| # export RESTIC_EXCLUDE_FILE=/opt/restic/restic.exclude | |
| # export RESTIC_BIN=/usr/bin/restic | |
| # export MAINTENANCE_HOUR=03 | |
| # export MAINTENANCE_STATE_FILE=/opt/restic/.last-maintenance |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /opt/restic/restic.exclude | |
| # | |
| # Copy to /opt/restic/restic.exclude and edit for this machine. install.sh | |
| # does this automatically on first install, and never touches the file | |
| # again on later updates. These are this machine's actual excludes as a | |
| # starting point — adjust paths for whichever user(s)/PC you're deploying to. | |
| # various big stuff | |
| /home/al/.local/share/Steam | |
| /home/al/.local/share/flatpak | |
| /home/al/.local/share/Trash | |
| /home/al/.local/share/bottles | |
| /home/al/.local/share/containers | |
| /home/al/winboat | |
| /home/al/Mods | |
| /home/al/Videos | |
| /home/al/Music/Libation | |
| /home/al/Downloads | |
| /home/al/.ftba | |
| /home/al/.vscode | |
| # programming stuff | |
| /home/al/.cargo/registry | |
| /home/al/.cargo/git | |
| /home/al/.gradle/caches | |
| /home/al/.m2/repository | |
| /home/al/.npm/_cacache | |
| /home/al/.ccache | |
| # Cache and log files | |
| /home/al/**/*Cache* | |
| /home/al/**/*cache* | |
| /home/al/.cache | |
| /home/al/.cache/** | |
| /home/al/.local/state | |
| /home/al/.local/state/** | |
| /home/al/.log | |
| /home/al/.log/** | |
| # Flatpak | |
| /home/al/.var/app/moe.launcher.sleepy-launcher/data/sleepy-launcher/ | |
| /home/al/.var/app/*/cache | |
| /home/al/.var/app/*/.cache | |
| /home/al/.var/app/*/tmp | |
| /home/al/.var/app/*/.tmp | |
| /home/al/.var/app/*/log | |
| /home/al/.var/app/*/.local/state |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # /opt/restic/restic.sh | |
| set -euo pipefail | |
| # systemd runs this without a login shell (and always as root, see below), | |
| # so $HOME is not guaranteed to be set. Default it before restic.env is | |
| # sourced, since restic.env itself may reference $HOME (e.g. BACKUP_TARGETS). | |
| : "${HOME:=/root}" | |
| source "${RESTIC_ENV_FILE:-/opt/restic/restic.env}" | |
| RESTIC_BIN="${RESTIC_BIN:-/usr/bin/restic}" | |
| RESTIC_EXCLUDE_FILE="${RESTIC_EXCLUDE_FILE:-/opt/restic/restic.exclude}" | |
| BACKUP_TARGETS="${BACKUP_TARGETS:-$HOME /etc}" | |
| MAINTENANCE_HOUR="${MAINTENANCE_HOUR:-03}" | |
| MAINTENANCE_STATE_FILE="${MAINTENANCE_STATE_FILE:-/opt/restic/.last-maintenance}" | |
| log() { echo "========= $* $(date) ========="; } | |
| trap 'echo "!!!!!!!! BACKUP FAILED (line $LINENO, exit $?) $(date) !!!!!!!!"' ERR | |
| # Backend-specific extra options. Only sftp needs a flag here (an explicit | |
| # private key); every other backend's credentials are read straight from | |
| # the environment by restic itself, so this stays empty for them. | |
| RESTIC_OPTS=() | |
| if [ -n "${SSH_KEY:-}" ]; then | |
| RESTIC_OPTS+=(-o "sftp.args=-i $SSH_KEY") | |
| fi | |
| # Each retention flag is independently optional: only pass the ones that are | |
| # set, rather than requiring all four. No default here (unlike RESTIC_BIN | |
| # etc.) — an unset KEEP_* means "don't enforce that cutoff", and if none are | |
| # set at all, forget/prune is skipped below rather than calling restic with | |
| # no retention policy. | |
| KEEP_OPTS=() | |
| [ -n "${KEEP_LAST:-}" ] && KEEP_OPTS+=(--keep-last "$KEEP_LAST") | |
| [ -n "${KEEP_DAILY:-}" ] && KEEP_OPTS+=(--keep-daily "$KEEP_DAILY") | |
| [ -n "${KEEP_WEEKLY:-}" ] && KEEP_OPTS+=(--keep-weekly "$KEEP_WEEKLY") | |
| [ -n "${KEEP_MONTHLY:-}" ] && KEEP_OPTS+=(--keep-monthly "$KEEP_MONTHLY") | |
| log START | |
| # Clear stale locks left by a crashed/killed run. Safe: without --remove-all | |
| # this only removes locks whose owning process is dead, never a live lock. | |
| # Non-fatal so a transient unlock error doesn't abort the run under `set -e`. | |
| "$RESTIC_BIN" -vv unlock "${RESTIC_OPTS[@]}" || true | |
| # Hourly: backup. --retry-lock waits out brief overlap with a live run. | |
| # BACKUP_TARGETS is intentionally unquoted so each path becomes a separate arg. | |
| "$RESTIC_BIN" -vv backup $BACKUP_TARGETS \ | |
| "${RESTIC_OPTS[@]}" \ | |
| --retry-lock "5m" \ | |
| --exclude-file="$RESTIC_EXCLUDE_FILE" | |
| # Daily (at or after MAINTENANCE_HOUR): forget old snapshots, prune, and | |
| # check the repository. These are I/O-heavy, so gate them to once a day | |
| # instead of every hour. Gated by >= and a last-run date file rather than an | |
| # exact hour match, so a machine that's off/asleep exactly at | |
| # MAINTENANCE_HOUR still catches up later the same day instead of skipping | |
| # maintenance entirely until tomorrow. The 10# prefixes force base-10 | |
| # parsing so hours like 08/09 aren't misread as invalid octal. | |
| today="$(date +%F)" | |
| last_maintenance="$(cat "$MAINTENANCE_STATE_FILE" 2>/dev/null || true)" | |
| if [ "$((10#$(date +%H)))" -ge "$((10#$MAINTENANCE_HOUR))" ] && [ "$last_maintenance" != "$today" ]; then | |
| if [ "${#KEEP_OPTS[@]}" -gt 0 ]; then | |
| "$RESTIC_BIN" -vv forget \ | |
| "${RESTIC_OPTS[@]}" \ | |
| --retry-lock "5m" \ | |
| "${KEEP_OPTS[@]}" \ | |
| --prune | |
| else | |
| log "no KEEP_* retention policy configured, skipping forget/prune" | |
| fi | |
| "$RESTIC_BIN" -vv check \ | |
| "${RESTIC_OPTS[@]}" \ | |
| --retry-lock "5m" | |
| # Only recorded after forget/check succeed: under set -e a failure exits | |
| # before this line, so the next run retries maintenance instead of | |
| # wrongly considering today done. | |
| echo "$today" > "$MAINTENANCE_STATE_FILE" | |
| fi | |
| log END |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment