close

DEV Community

Cover image for Linux Backup Made Simple: Automate Incremental System Snapshots with Restic
Tao Ran
Tao Ran

Posted on • Originally published at alphatango086.Medium

Linux Backup Made Simple: Automate Incremental System Snapshots with Restic

A single Bash script, a USB drive, and 30 seconds a day. No cloud. No subscriptions. No excuses.

You have spent months tweaking your Linux environment. The perfect i3 config, the custom kernel parameters, the Docker containers you curated one by one. Then one day — a power surge fries your NVMe. A bad rm -rf. A failed dist-upgrade. And it's all gone.

Most Linux users either don't back up at all, or rely on manual rsync and tar commands they run twice a year. The good news: you can set up fully automated, incremental, encrypted system snapshots with a single script and a USB drive — no cloud subscriptions, no complex configuration, no vendor lock-in.

In this guide I'll show you how to use restic — an open-source backup tool with built-in deduplication, client-side encryption, and snapshot management — paired with a Bash script that handles the real-world edge cases for you: detecting external mounts to avoid wasteful backups, skipping caches that would bloat your repository, and initializing itself on first run.


Why Restic?

Most Linux users fall into one of two camps: they either don't back up at all, or they use rsync / tar because that's what they have always used. Both are risky for different reasons. Here is how restic compares:

Tool Incremental Dedup Encryption Snapshot Mgmt Restore UX
rsync partial no no no manual
tar no no no no manual
duplicity yes no yes basic slow on long chains
borg yes yes yes yes requires borg binary
timeshift yes no no yes GUI-focused, tied to local disk
restic yes yes yes yes single binary, any target

Here is why these differences matter in practice:

True Incremental Backups

Restic chunks your files into content-addressed blobs. When you run a backup, it only uploads chunks it hasn't seen before. The second backup of a 200GB system where you changed three config files? It takes seconds, not hours.

Global Deduplication

Have the same 2GB ISO sitting in three different directories? Restic stores it once. The same Python virtual environment duplicated across five projects? One copy. This matters especially when your USB drive is finite.

Client-Side Encryption

Your backup repository is encrypted before a single byte leaves your machine. Lose the USB drive on a train? Without the password, the data is meaningless noise. Restic uses AES-256 in CTR mode with Poly1305-AES for authentication.

Snapshots, Not Archives

Every backup is a named, timestamped snapshot. You can list them, diff them, mount them as a FUSE filesystem, or restore individual files from any point in time. This is closer to what you would expect from a ZFS snapshot than a tarball.

Single Static Binary

No daemon. No database. No kernel module. Restic is one Go binary. Copy it to any machine and it works. This also makes it trivially deployable in rescue environments — just scp the binary and go.


The Script: Automation with Intelligence

A backup tool is only as good as your willingness to run it. If it requires you to remember which directories to exclude, or mounts to skip, or flags to pass, you will eventually stop running it.

The script tackles four real-world problems:

1. Sensible Exclusions Out of the Box

The script ships with a curated set of exclusions that skip directories you would never want in a system backup:

EXCLUDES=(
    --exclude /proc --exclude /sys --exclude /dev
    --exclude /tmp --exclude /run --exclude /mnt --exclude /media
    --exclude /var/tmp --exclude /lost+found --exclude /.snapshots
    --exclude '/home/*/.cache'
    --exclude '/home/*/.npm'
    --exclude '/home/*/.local/share/Trash'
    --exclude /var/cache/apt/archives
)
Enter fullscreen mode Exit fullscreen mode

A few of these deserve explanation:

  • /home/*/.cache — This is the big one. Browser caches, Go build artifacts, pip wheels, and a thousand other things apps dump here. On a well-used system this can easily be 5–10 GB of data you will never need to restore.
  • /home/*/.npm — Node.js package cache. A single npm install rebuilds it.
  • /home/*/.local/share/Trash — The trash bin. You deleted it once already.
  • /var/cache/apt/archives — Downloaded .deb packages. apt update fetches fresh ones.

Using bash arrays ("${EXCLUDES[@]}") instead of a plain string ensures patterns like /home/*/.cache survive shell expansion intact and are interpreted correctly by restic.

2. Auto-Detect External Mounts

On top of the static exclusion list, the script dynamically detects remote filesystems so you never accidentally pull a NAS into your backup. It parses /proc/mounts and appends matches to the same array:

while read -r _ mp fs_type _; do
    case "$fs_type" in
        cifs|nfs|nfs4|fuse.sshfs|fuse.rclone)
            EXCLUDES+=(--exclude "$mp")
            ;;
    esac
done < /proc/mounts
Enter fullscreen mode Exit fullscreen mode

This means you can mount and unmount NAS shares, add new ones, or switch between SMB and NFS, and the script will never accidentally pull them in. Each detected mount is logged so you can verify what was excluded.

3. First Run vs. Daily Run

The script has exactly two paths through its logic:

  • No repository yet → initialize a new encrypted restic repository on the USB drive, then run the first backup.
  • Repository exists → run an incremental backup. Only changed chunks are transmitted.

No separate init step. No configuration file to maintain. Plug in the USB, run the script, and it figures out what to do.

if [ ! -f "${REPO}/config" ]; then
    restic init --repo "$REPO"
fi
restic backup / --repo "$REPO" "${EXCLUDES[@]}"
Enter fullscreen mode Exit fullscreen mode

4. Format Flexibility

Three modes for different needs:

./backup.sh           # incremental — what you run every day
./backup.sh --dry     # preview what would change, no data written
./backup.sh --full    # force re-read of all files
Enter fullscreen mode Exit fullscreen mode

The --dry mode is particularly useful before a system upgrade — see exactly which files have changed since your last backup. The --full mode is for when you suspect filesystem corruption or bit rot and want a fresh scan of every file.


The Full Script

Copy it, adjust the two variables at the top, and you are ready to go:

#!/bin/bash
# ============================================================
# Restic incremental backup script — local machine
# Usage: ./backup.sh          # incremental backup
#        ./backup.sh --dry    # preview changes
#        ./backup.sh --full   # force full scan
# ============================================================
set -euo pipefail

# ---- Config: customize these two lines ----
USB="/media/$USER/backup-disk"    # USB mount point
REPO="${USB}/my-laptop"           # subdirectory (unique per machine)

TIMESTAMP_FILE="${REPO}/LAST_BACKUP"
LOGFILE="${HOME}/.local/state/backup.log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"; }
die()  { log "ERROR: $*"; exit 1; }

mkdir -p "$(dirname "$LOGFILE")"

# ---- Dependencies ----
command -v restic >/dev/null 2>&1 || die "restic not installed. Run: sudo apt install restic"

# ---- USB mount check ----
mountpoint -q "$USB" || die "USB not mounted: $USB"

# ---- Build exclude list ----
EXCLUDES=(
    --exclude /proc --exclude /sys --exclude /dev
    --exclude /tmp --exclude /run --exclude /mnt --exclude /media
    --exclude /var/tmp --exclude /lost+found --exclude /.snapshots
    --exclude '/home/*/.cache'
    --exclude '/home/*/.npm'
    --exclude '/home/*/.local/share/Trash'
    --exclude /var/cache/apt/archives
)

# Auto-detect CIFS / NFS / SSHFS external mounts
while read -r _ mp fs_type _; do
    case "$fs_type" in
        cifs|nfs|nfs4|fuse.sshfs|fuse.rclone)
            EXCLUDES+=(--exclude "$mp")
            log "Excluding external mount: $mp ($fs_type)"
            ;;
    esac
done < /proc/mounts

# ---- Repo init ----
if [ ! -f "${REPO}/config" ]; then
    log "Repo not found, initializing restic repository..."
    restic init --repo "$REPO" || die "Repo init failed"
    log "Repo initialized."
else
    log "Repo exists, running incremental backup."
fi

# ---- Arguments ----
RESTIC_ARGS=(
    --verbose
    --limit-upload 50000
    --limit-download 50000
)

case "${1:-}" in
    --dry|--dry-run)
        log "=== DRY-RUN mode ==="
        restic backup / \
            --repo "$REPO" \
            "${EXCLUDES[@]}" \
            "${RESTIC_ARGS[@]}" \
            --dry-run
        log "Dry-run complete."
        exit 0
        ;;
    --full)
        log "Forcing full scan."
        RESTIC_ARGS+=(--force)
        ;;
esac

# ---- Backup ----
log "Starting backup / → $REPO ..."
restic backup / \
    --repo "$REPO" \
    "${EXCLUDES[@]}" \
    "${RESTIC_ARGS[@]}" \
    2>&1 | tee -a "$LOGFILE"

# ---- Timestamp ----
date '+%Y-%m-%d %H:%M:%S' > "$TIMESTAMP_FILE"
log "Backup complete. Timestamp: $(cat "$TIMESTAMP_FILE")"

# ---- Maintenance reminder ----
SNAP_COUNT=$(restic snapshots --repo "$REPO" 2>/dev/null | grep -c '^[a-f0-9]' || echo "?")
log "Snapshot count: $SNAP_COUNT"
log "Tip: restic forget --repo $REPO --keep-daily 14 --keep-monthly 6 --keep-yearly 5 --prune"
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Setup

Prerequisites

  • A USB drive (preferably SSD) with enough capacity. Format it as ext4 for best performance and to preserve Linux permissions.
  • restic installed on your machine (sudo apt install restic on Debian/Ubuntu).

Step 1: Find Your USB

lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,LABEL
Enter fullscreen mode Exit fullscreen mode

Look for your USB drive in the output. Note the mount point — typically /media/$USER/<label>. Set the USB variable in the script to match.

Step 2: Save the Script

Copy the script above into a file and make it executable:

chmod +x backup.sh
Enter fullscreen mode Exit fullscreen mode

If you have multiple machines, give each a different REPO name (e.g., my-laptop, my-desktop, my-server). This keeps their backups in separate subdirectories on the same USB drive.

Step 3: First Run

./backup.sh
Enter fullscreen mode Exit fullscreen mode

Restic will prompt you to set a password for the repository. Do not lose this password. Write it down. Store it in your password manager. Without it, the backup is unrecoverable. There is no "reset password" button.

The first run will take a while — it reads your entire filesystem. On a 200GB SSD over USB 3.0, expect roughly 20–40 minutes. Subsequent runs take seconds to a few minutes.

Step 4: Verify

restic snapshots --repo "$USB/my-laptop"
Enter fullscreen mode Exit fullscreen mode

You should see your first snapshot, timestamped. Run ./backup.sh again a few hours later and you will see a second snapshot appear. The data stored on disk will barely grow because only changed files were uploaded.

Step 5: Automate (Optional but Recommended)

Add a cron job or systemd timer:

# crontab -e
0 20 * * * /home/$USER/scripts/backup.sh >> /home/$USER/.local/state/backup.log 2>&1
Enter fullscreen mode Exit fullscreen mode

This runs the backup every evening at 8 PM. Since the script uses restic's --limit-upload flag, it won't saturate your USB bandwidth even on slower drives.


Maintenance and Restore

Cleaning Up Old Snapshots

Snapshots accumulate. After a few months, you might have hundreds. Restic's forget policy lets you keep a sensible rotation:

restic forget --repo "$USB/my-laptop" \
    --keep-daily 14 \
    --keep-monthly 6 \
    --keep-yearly 5 \
    --prune
Enter fullscreen mode Exit fullscreen mode

This keeps: every snapshot from the last 14 days, one per month for the last 6 months, and one per year for the last 5 years. Everything else is pruned and the underlying data blobs are garbage-collected.

Restoring Your System

Full system restore to a new disk:

restic restore latest --repo "$USB/my-laptop" --target /mnt/new-disk
Enter fullscreen mode Exit fullscreen mode

Single file restore:

restic restore latest --repo "$USB/my-laptop" --target / --include /home/$USER/.bashrc
Enter fullscreen mode Exit fullscreen mode

Restore from a specific date:

restic snapshots --repo "$USB/my-laptop"              # find the snapshot ID
restic restore <snapshot-id> --repo "$USB/my-laptop" --target /path
Enter fullscreen mode Exit fullscreen mode

Checking Repository Health

Run periodically (monthly is fine):

restic check --repo "$USB/my-laptop"               # quick structural check
restic check --repo "$USB/my-laptop" --read-data   # thorough, reads all data
Enter fullscreen mode Exit fullscreen mode

Mounting Snapshots as a Filesystem

You can browse your snapshots without restoring:

restic mount --repo "$USB/my-laptop" /mnt/restic
ls /mnt/restic/snapshots/
Enter fullscreen mode Exit fullscreen mode

This is invaluable for comparing versions or grabbing a file you deleted last week without doing a full restore.


Summary

You don't need a complex backup strategy. You need one USB drive, one script, and one habit.

Restic gives you enterprise-grade backup features — deduplication, encryption, snapshot management — in a tool that is genuinely simple to use. The script wraps it in automation that handles the real-world edge cases: external mounts, cache directories, first-run initialization, and daily incrementals.

Set it up once. Run it often. The next time your system breaks, you will be back up and running in the time it takes to restore a snapshot, not the time it takes to rebuild from memory.

Top comments (0)