Manual backups won't last long in production. This episode assembles a safe backup+forget+prune script with `set -euo pipefail`, a lock file, and logging, then schedules it with cron and a systemd timer, including verifying the backup result at the end of every run.

So far you have typed every command by hand — and that won't survive when you have 20 servers. Backups that depend on human memory are bound to fail: "forgot", "I'll do it tomorrow", "I'm busy right now". This episode makes backups run themselves: a safe, scheduled script that leaves a trail behind.
Automation is not just a cron restic backup. It is a chain: backup → verify → forget → prune, with guards at every link.
#!/usr/bin/env bash
set -euo pipefail-e: stop on any error.-u: error on undefined variables.-o pipefail: failures in a pipeline are not hidden.Without all three, a failed backup can be "missed" while cron still reports success — the most common cause of fake backups.
#!/usr/bin/env bash
set -euo pipefail
export RESTIC_REPOSITORY=/backup/restic
export RESTIC_PASSWORD="$(cat /etc/restic/passphrase)"
export TMPDIR=/var/tmp
exec 2>>/var/log/restic/backup.log
echo "== backup start: $(date -Iseconds) =="
restic backup /home/user /etc \
--tag daily \
--exclude-file=/etc/restic/excludes.txt
restic check --quiet
restic forget --keep-daily 7 --keep-monthly 6 --prune
echo "== backup done: $(date -Iseconds) =="The flow above guarantees the correct logical order: backup first, verify integrity, then remove history — never forget before the data is verified safe.
Long-running backups can overlap with the next scheduled run. Use a lock:
LOCKFILE=/var/run/restic-backup.lock
exec 9>"$LOCKFILE"
flock -n 9 || { echo "backup already running, skip"; exit 0; }flock -n locks fd 9 non-blockingly; if the lock is already held by another process, the script exits quietly instead of stacking two heavy processes.
Classic crontab — backup every night at 02:00:
0 2 * * * root /usr/local/bin/backup-restic.shFormat: minute, hour, day-of-month, month, day-of-week. Other examples: every 4 hours 0 */4 * * *, every week at 01:00 0 1 * * 0.
Note
Don't use crontab -e for system jobs — use a file in /etc/cron.d/ with an explicit user (the user column) so it is consistent and versionable.
Cron has no dependency handling; a systemd timer can make sure a backup only runs after a mount/storage is ready. Two files are needed:
[Unit]
Description=Backup data with restic
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-restic.sh[Unit]
Description=Schedule nightly restic backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.targetEnable and start it:
sudo systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timerPersistent=true matters: if the machine is off at the scheduled time, the timer runs immediately after it boots — cron does not do this.
A finished backup does not mean success. Add verification at the end of the script:
restic snapshots --latest --json | jq -r '.[0].id'restic snapshots --latest --json produces JSON that can be parsed for monitoring (episode 20). Full verification with --read-data is scheduled separately (episode 11) — for daily runs, restic check plus the snapshot showing up is enough.
set -euo pipefail is mandatory in every script — failures must not stay silent.flock) prevents overlapping backups.Persistent).restic check and confirm the snapshot appears./var/log/restic/ file for audit and alerting.In the next episode, episode 10, we handle the most finicky data: consistent database & application backups — why a filesystem snapshot is not a logical DB backup, the use of WAL, pre/post hooks with pg_dump/mysqldump to a staging dir, and the PostgreSQL, MySQL/MariaDB, and application file cases.