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
)
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 singlenpm installrebuilds it. -
/home/*/.local/share/Trash— The trash bin. You deleted it once already. -
/var/cache/apt/archives— Downloaded.debpackages.apt updatefetches 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
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[@]}"
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
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"
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 resticon Debian/Ubuntu).
Step 1: Find Your USB
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,LABEL
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
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
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"
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
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
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
Single file restore:
restic restore latest --repo "$USB/my-laptop" --target / --include /home/$USER/.bashrc
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
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
Mounting Snapshots as a Filesystem
You can browse your snapshots without restoring:
restic mount --repo "$USB/my-laptop" /mnt/restic
ls /mnt/restic/snapshots/
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.













