Skip to content
Commitpedia
Log in Join & edit

restic backup wrapper with tagging, retention and a weekly check

A bash wrapper that runs restic backup on a fixed list of paths, applies a keep-daily/weekly/monthly policy with forget and prune, runs a sampled integrity check on Sundays and logs everything.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of restic backup wrapper with tagging, retention and a weekly check: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
bash
Licence
CC0-1.0

What this changes

Writes a new snapshot to the restic repository, then runs restic forget --prune, which permanently deletes snapshots outside the keep policy and the data only they referenced. Appends to /var/log/restic-backup.log. Does not modify the source files.

How to undo it

Snapshots removed by forget and prune cannot be brought back. Restore files from any remaining snapshot with restic snapshots and restic restore ID --target DIR. To stop retention, remove the forget call or set the keep values higher before the next run.

restic itself is one binary with no server component. The missing piece is a small wrapper that fixes the repository, the paths, the retention and the logging in one place so that every run, by hand or from a timer, does the same thing. This script is that wrapper.

One-time setup

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo mkdir -p /etc/restic
openssl rand -base64 32 | sudo tee /etc/restic/password >/dev/null
sudo chmod 600 /etc/restic/password
printf '%s\n' '/home/*/.cache' '/var/www/*/cache' '*.tmp' | sudo tee /etc/restic/excludes.txt
export RESTIC_REPOSITORY=/srv/backup/restic
export RESTIC_PASSWORD_FILE=/etc/restic/password
restic init

Copy the password file somewhere that is not this machine (a password manager is fine). Without it the repository is unreadable, by you or anyone else. For a remote repository set RESTIC_REPOSITORY to an sftp:user@host:/path, s3: or rclone: URL; restic supports them all with the same commands.

The script

Save as /usr/local/bin/restic-backup.sh, adjust SOURCES and the keep values, and chmod 755 it.

#!/usr/bin/env bash
set -euo pipefail

export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-/srv/backup/restic}"
export RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-/etc/restic/password}"
SOURCES=(/etc /home /var/www)
EXCLUDE_FILE="/etc/restic/excludes.txt"
KEEP_DAILY=7
KEEP_WEEKLY=4
KEEP_MONTHLY=6
TAG="nightly"
LOG="/var/log/restic-backup.log"

log() { printf '%s %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG"; }

if [ ! -r "$RESTIC_PASSWORD_FILE" ]; then
  log "password file $RESTIC_PASSWORD_FILE is missing or unreadable"
  exit 1
fi

if ! restic cat config >/dev/null 2>&1; then
  log "repository $RESTIC_REPOSITORY is not initialised, run: restic init"
  exit 1
fi

EXCLUDE_ARGS=()
if [ -r "$EXCLUDE_FILE" ]; then
  EXCLUDE_ARGS=(--exclude-file "$EXCLUDE_FILE")
fi

log "backup start: ${SOURCES[*]}"
restic backup "${SOURCES[@]}" \
  --tag "$TAG" \
  --exclude-caches \
  --one-file-system \
  "${EXCLUDE_ARGS[@]}" 2>&1 | tee -a "$LOG"
status=${PIPESTATUS[0]}
if [ "$status" -eq 3 ]; then
  log "backup finished with warnings: some files could not be read"
elif [ "$status" -ne 0 ]; then
  log "backup FAILED with exit code $status"
  exit "$status"
fi

log "apply retention: daily=$KEEP_DAILY weekly=$KEEP_WEEKLY monthly=$KEEP_MONTHLY"
restic forget \
  --tag "$TAG" \
  --group-by host,paths \
  --keep-daily "$KEEP_DAILY" \
  --keep-weekly "$KEEP_WEEKLY" \
  --keep-monthly "$KEEP_MONTHLY" \
  --prune 2>&1 | tee -a "$LOG"

if [ "$(date +%u)" -eq 7 ]; then
  log "weekly integrity check"
  restic check --read-data-subset=5% 2>&1 | tee -a "$LOG"
fi

log "done, latest snapshots:"
restic snapshots --latest 1 --compact 2>&1 | tee -a "$LOG"

How it works

  • restic cat config is a cheap way to confirm the repository exists and the password is right before anything else runs.
  • --exclude-caches skips directories marked with a CACHEDIR.TAG file; --one-file-system keeps the backup from wandering into mounted filesystems such as /proc or a NAS mount.
  • restic exits with code 3 when some files could not be read (for example a file that vanished mid-run). The script logs that as a warning and continues, because the snapshot is still valid; any other non-zero code aborts before retention runs, so a failed backup never triggers deletion of old snapshots.
  • forget --group-by host,paths applies the keep rules per host and path set, so a second machine backing up into the same repository does not consume the first machine’s daily slots.
  • Careful: --prune permanently deletes data that no kept snapshot references. Run the forget line with --dry-run once to see what would be removed before you trust the numbers.
  • restic check --read-data-subset=5% reads a random 5 percent of the pack files every Sunday and verifies their hashes, catching bit rot on the backup medium without reading the entire repository.

Run it and schedule it

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo /usr/local/bin/restic-backup.sh
tail -n 30 /var/log/restic-backup.log
restic snapshots

To run it nightly, create a restic-backup.service with Type=oneshot and a matching .timer, exactly as in Schedule PostgreSQL and MySQL dumps with systemd timers; set the environment variables in the service with Environment= lines or keep them in the script as shown. On macOS, a launchd plist with StartCalendarInterval does the same job.

If a run is interrupted

A killed run can leave a stale lock. restic unlock removes locks that no running process holds. If restic check reports errors, run restic repair index and then check again; if it still fails, consult the restic troubleshooting guide before pruning anything.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0059 Incremental snapshot backups with rsync A bash script that copies a folder into dated snapshots with rsync, hard-links unchanged files to the previous snapshot to save space,… Advanced · Step by step · Linux, macOS Script Backups & syncing · Shell scripting 15 Oct 2026
  • 0336 Back up and restore a Docker named volume to a tar archive A bash script that copies the contents of a named Docker volume into a .tar.gz on the host, and restores an archive… Intermediate · Step by step · Linux, macOS Script Backup tools · Docker basics 15 Oct 2026
  • 0363 Restore drill: prove your backup works in 15 minutes A repeatable procedure to restore one file and one database from your newest backup into a scratch location, compare the result with… Intermediate · Step by step · Linux, macOS Article Backup tools · Strategy & restore drills 15 Oct 2026
  • 0364 Schedule PostgreSQL and MySQL dumps with systemd timers Write one dump script for PostgreSQL and MySQL, run it from a systemd service and timer instead of cron, keep two weeks… Intermediate · Step by step · Linux Article Backup tools · PostgreSQL 15 Oct 2026
  • 0365 Proxmox Backup Server: first setup and first restore Install Proxmox Backup Server, create a datastore and a backup user, connect it to Proxmox VE as storage, run the first VM… Intermediate · Step by step · Linux Article Backup & restore · Backup tools 15 Oct 2026
  • 0367 verify-latest-backup: alert when the newest dump is too old or too small A read-only bash check that finds the newest file in a backup directory, compares its age and size against limits, optionally tests… Beginner · Step by step · Linux Script Backups & syncing · Strategy & restore drills 15 Oct 2026

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +5,166 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?