Setup time: ~45 minutes with Claude Code assistance (not counting initial backup upload, which runs in the background and may take hours/days depending on your data size and internet speed).
A practical guide to backing up your computer to the cloud using restic and Backblaze B2. Your files are encrypted before leaving your machine, deduplicated to save space, and stored offsite for ~$6/TB/month.
Why This Setup?
- Encrypted: Your data is encrypted with a password before upload. Backblaze can’t read it.
- Deduplicated: Only changed portions of files are uploaded. A 10GB file with 1MB of changes = 1MB upload.
- Versioned: Keep 30 days of snapshots. Accidentally delete something? Restore from yesterday.
- Cheap: ~$6/TB/month. 100GB costs ~60 cents/month.
- Cross-platform: Same tool works on Windows, macOS, and Linux.
What about OneDrive/iCloud/Google Drive?
Those sync services are great for access across devices, but they’re not proper backups:
- Delete a file? It’s deleted everywhere.
- Ransomware encrypts your files? Synced everywhere.
- They don’t keep long version histories.
Use restic + B2 in addition to your sync service, not instead of it.
Overview
- Create a Backblaze B2 account and bucket
- Install restic
- Configure what to back up (and what to skip)
- Run your first backup
- Automate daily backups
Pick your OS: Windows 11 | macOS | Linux
Part 1: Backblaze B2 Setup (All Platforms)
1.1 Create Account
- Go to backblaze.com/b2
- Sign up for B2 Cloud Storage (not “Personal Backup” โ that’s a different product)
- Verify your email
1.2 Create Bucket
Buckets โ Create a Bucket
Settings:
- Bucket Name:
mybackup-abc123(must be globally unique โ add random characters) - Files in Bucket: Private
- Default Encryption: Disable (restic handles encryption client-side)
- Object Lock: Disable
- Bucket Name:
Note the S3 Endpoint shown (e.g.,
s3.us-west-004.backblazeb2.com)
1.3 Set Lifecycle Policy
- Click your bucket โ Lifecycle Settings
- Set to: “Keep only the last version of the file”
Why this matters: When using B2’s S3-compatible API, restic “hides” old file versions rather than deleting them. B2 keeps hidden versions indefinitely by default, which wastes storage. This lifecycle rule automatically deletes hidden versions after one day, freeing up space. (Restic handles its own versioning via snapshots โ B2’s file versioning is redundant.)
1.4 Create Application Key (S3-Compatible)
We need S3-compatible credentials. See Backblaze’s S3-compatible API documentation for details.
App Keys โ Add a New Application Key
Settings:
- Name:
restic-backup - Allow access to Bucket(s): Select your specific bucket
- Type of Access: Read and Write
- Allow List All Bucket Names: Unchecked
- Name:
SAVE THESE IMMEDIATELY (shown only once):
- keyID โ this becomes your
AWS_ACCESS_KEY_ID - applicationKey โ this becomes your
AWS_SECRET_ACCESS_KEY
- keyID โ this becomes your
Store these in your password manager. You’ll use them with AWS environment variable names (that’s how S3-compatible APIs work).
Why S3-Compatible Mode?
Backblaze B2 offers two connection methods: native B2 (b2:bucket) and S3-compatible (s3:endpoint/bucket). This guide uses S3-compatible mode because restic’s documentation recommends it:
“Due to issues with error handling in the current B2 library that restic uses, the recommended way to utilise Backblaze B2 is by using its S3-compatible API.”
The S3 mode has better error handling and is more reliable for automated backups.
Windows 11
2.1 Install Restic
Option A: Scoop
# Install Scoop if you don't have it
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
# Install restic
scoop install restic
Option B: WinGet (recommended for system-wide backups)
winget install --exact --id restic.restic --scope Machine
This installs to %ProgramFiles% and adds restic to your PATH.
Option C: Manual download
- Download from github.com/restic/restic/releases
- Get the
restic_X.X.X_windows_amd64.zipfile - Extract
restic.exetoC:\Program Files\restic\ - Add
C:\Program Files\restic\to your PATH
2.2 Create Configuration
Create a folder for your backup config:
mkdir "$env:USERPROFILE\.config\restic"
Create the environment file %USERPROFILE%\.config\restic\env.ps1:
$env:RESTIC_REPOSITORY = "s3:s3.us-west-004.backblazeb2.com/YOUR-BUCKET-NAME"
$env:RESTIC_PASSWORD = "YOUR_SECURE_PASSWORD_HERE"
$env:AWS_ACCESS_KEY_ID = "YOUR_B2_KEY_ID"
$env:AWS_SECRET_ACCESS_KEY = "YOUR_B2_APPLICATION_KEY"
Replace:
us-west-004with your region from the bucket pageYOUR-BUCKET-NAMEwith your actual bucket nameYOUR_SECURE_PASSWORD_HEREwith a strong password (this encrypts your backups โ save it in your password manager)- The AWS variables with your B2 credentials
Restrict the file to your user account. The file now contains your repo password and B2 keys in plaintext โ anyone able to read it has full access to your backups. Remove inherited permissions and grant only your user:
icacls "$env:USERPROFILE\.config\restic\env.ps1" /inheritance:r /grant:r "$($env:USERNAME):(R,W)"
2.3 Create Exclude File
Create %USERPROFILE%\.config\restic\excludes.txt:
# Windows system files
$RECYCLE.BIN
System Volume Information
pagefile.sys
hiberfil.sys
swapfile.sys
*.tmp
*.temp
~$*
# Caches
AppData\Local\Temp
AppData\Local\Microsoft\Windows\INetCache
AppData\Local\Microsoft\Windows\Explorer\thumbcache*
AppData\Local\Packages\*\LocalCache
AppData\Local\Packages\*\TempState
# Browser caches (bookmarks/passwords ARE backed up)
AppData\Local\Google\Chrome\User Data\*\Cache
AppData\Local\Google\Chrome\User Data\*\Code Cache
AppData\Local\Google\Chrome\User Data\*\GPUCache
AppData\Local\BraveSoftware\Brave-Browser\User Data\*\Cache
AppData\Local\BraveSoftware\Brave-Browser\User Data\*\Code Cache
AppData\Local\Microsoft\Edge\User Data\*\Cache
AppData\Local\Mozilla\Firefox\Profiles\*\cache2
# Development
node_modules
.venv
__pycache__
*.pyc
# Large app caches
AppData\Local\Steam\steamapps
AppData\Local\Discord\Cache
AppData\Roaming\Spotify\Data
# OneDrive (already in cloud)
OneDrive
2.4 Initialise Repository
Open PowerShell:
# Load environment
. "$env:USERPROFILE\.config\restic\env.ps1"
# Initialise (one-time setup)
restic init
You’ll see a repository ID โ save this alongside your password.
2.5 First Backup
Recommended: Exclude large files
Add --exclude-larger-than 300M to skip files over 300MB. Large files (videos, ISOs, game assets) are often replaceable and expensive to store in the cloud. Adjust the threshold to your needs.
restic backup $env:USERPROFILE --exclude-file="$env:USERPROFILE\.config\restic\excludes.txt" --exclude-larger-than 300M --verbose
This single flag can dramatically reduce your backup size and B2 costs.
Know what this skips. A 300MB threshold silently excludes most 4K video clips, many stitched panoramas, Lightroom catalogues, and Photos/Aperture libraries (which are packages that often run tens of gigabytes). If any of those are irreplaceable to you, raise the threshold or back them up separately โ the backup will otherwise finish cleanly without them and you won’t notice they’re missing until you need them.
Optional: Test with a dry run first
. "$env:USERPROFILE\.config\restic\env.ps1"
restic backup $env:USERPROFILE --exclude-file="$env:USERPROFILE\.config\restic\excludes.txt" --dry-run -vv
This shows what would be backed up without uploading anything.
Run the actual backup:
# Load environment
. "$env:USERPROFILE\.config\restic\env.ps1"
# Back up your user folder
restic backup $env:USERPROFILE --exclude-file="$env:USERPROFILE\.config\restic\excludes.txt" --verbose
First backup may take hours depending on data size and upload speed. Subsequent backups only upload changes.
2.6 Automate with Task Scheduler
Create the backup script %USERPROFILE%\.config\restic\backup.ps1:
# Load environment
. "$env:USERPROFILE\.config\restic\env.ps1"
# Run backup (skip files >300MB)
restic backup $env:USERPROFILE --exclude-file="$env:USERPROFILE\.config\restic\excludes.txt" --exclude-larger-than 300M --quiet
$backupExit = $LASTEXITCODE
# Only forget old snapshots if backup succeeded.
# Exit 0 = clean success, exit 3 = partial (some files unreadable, snapshot still created).
# Any other code means no usable snapshot โ don't touch retention.
if ($backupExit -eq 0 -or $backupExit -eq 3) {
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6
} else {
Write-Error "Backup failed with exit code $backupExit โ skipping forget."
exit $backupExit
}
Note: this script runs forget (cheap metadata-only) but not prune. Pruning rewrites pack files and downloads data from B2 to do so โ running it daily is wasteful and can add meaningful egress cost on larger repos. Schedule pruning separately (see below).
Create the scheduled task:
- Open Task Scheduler (search in Start menu)
- Click Create Task (not “Create Basic Task”)
- General tab:
- Name:
Restic Backup - Check “Run whether user is logged on or not”
- Check “Run with highest privileges”
- Name:
- Triggers tab โ New:
- Begin the task: On a schedule
- Daily, at 2:00 PM (or whenever your computer is usually on)
- Check “Enabled”
- Actions tab โ New:
- Action: Start a program
- Program:
powershell.exe - Arguments:
-ExecutionPolicy Bypass -File "%USERPROFILE%\.config\restic\backup.ps1"
- Conditions tab:
- Uncheck “Start only if the computer is on AC power” (if you want laptop backups on battery)
- Settings tab:
- Check “Run task as soon as possible after a scheduled start is missed”
- Click OK, enter your Windows password
Create a separate weekly prune script %USERPROFILE%\.config\restic\prune.ps1:
. "$env:USERPROFILE\.config\restic\env.ps1"
restic prune
Schedule this as a second task using the same Task Scheduler process, but weekly (e.g. Sunday 4:00 AM) instead of daily, pointing at prune.ps1.
2.7 Verify It’s Working
. "$env:USERPROFILE\.config\restic\env.ps1"
restic snapshots
You should see your backup snapshots listed.
macOS
3.1 Install Restic
Option A: Homebrew (recommended)
brew install restic
Option B: Manual download
# Download latest release
curl -LO https://github.com/restic/restic/releases/download/v0.17.3/restic_0.17.3_darwin_arm64.bz2
# Extract (use darwin_amd64 for Intel Macs)
bunzip2 restic_*.bz2
chmod +x restic_*
sudo mv restic_* /usr/local/bin/restic
3.2 Create Configuration
mkdir -p ~/.config/restic
Create ~/.config/restic/env.sh:
export RESTIC_REPOSITORY="s3:s3.us-west-004.backblazeb2.com/YOUR-BUCKET-NAME"
export RESTIC_PASSWORD="YOUR_SECURE_PASSWORD_HERE"
export AWS_ACCESS_KEY_ID="YOUR_B2_KEY_ID"
export AWS_SECRET_ACCESS_KEY="YOUR_B2_APPLICATION_KEY"
Secure the file:
chmod 600 ~/.config/restic/env.sh
Replace the placeholder values with your actual credentials.
3.3 Create Exclude File
Create ~/.config/restic/excludes.txt:
# macOS system
.Trash
.Spotlight-V100
.fseventsd
.DS_Store
.AppleDouble
.LSOverride
._*
# Caches
Library/Caches
Library/Logs
Library/Application Support/Google/Chrome/*/Cache
Library/Application Support/Google/Chrome/*/Code Cache
Library/Application Support/BraveSoftware/Brave-Browser/*/Cache
Library/Application Support/BraveSoftware/Brave-Browser/*/Code Cache
Library/Application Support/Firefox/Profiles/*/cache2
Library/Application Support/Slack/Cache
Library/Application Support/Spotify/PersistentCache
Library/Application Support/discord/Cache
Library/Containers/*/Data/Library/Caches
# Development
node_modules
.venv
__pycache__
*.pyc
.npm
.cargo
# Large/replaceable
Downloads/*.dmg
Downloads/*.iso
Movies
Music/Music/Media
# NOTE: Photos libraries (*.photoslibrary) are NOT excluded.
# If you use iCloud Photos and your library is fully synced in the cloud,
# uncomment the next line. Otherwise keep it commented โ restic will back up
# the package, which may be large but is often the most valuable data on the machine.
# *.photoslibrary
# iCloud (already in cloud)
Library/Mobile Documents
3.4 Initialise Repository
source ~/.config/restic/env.sh
restic init
Save the repository ID shown.
3.5 First Backup
Recommended: Exclude large files
Add --exclude-larger-than 300M to skip files over 300MB. Large files (videos, ISOs, game assets) are often replaceable and expensive to store in the cloud.
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --verbose
This single flag can dramatically reduce your backup size and B2 costs.
Know what this skips. A 300MB threshold silently excludes most 4K video clips, many stitched panoramas, Lightroom catalogues, and Photos/Aperture libraries (which are packages that often run tens of gigabytes). If any of those are irreplaceable to you, raise the threshold or back them up separately โ the backup will otherwise finish cleanly without them and you won’t notice they’re missing until you need them.
Optional: Test with a dry run first
source ~/.config/restic/env.sh
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --dry-run -vv
This shows what would be backed up without uploading anything.
Run the actual backup:
source ~/.config/restic/env.sh
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --verbose
3.6 Automate with launchd
Create the backup script ~/.config/restic/backup.sh:
#!/bin/bash
set -euo pipefail
# Homebrew (and thus restic) lives outside launchd's default PATH,
# especially on Apple Silicon at /opt/homebrew/bin. Without this export
# the scheduled run fails silently with "restic: command not found".
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
# shellcheck source=/dev/null
source ~/.config/restic/env.sh
# Run backup (skip files >300MB). Don't exit on non-zero here โ we want to
# inspect the code and only skip forget on a hard failure.
set +e
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --quiet
rc=$?
set -e
# Only forget old snapshots if backup succeeded.
# Exit 0 = clean success, exit 3 = partial (some files unreadable, snapshot still created).
# Any other code means no usable snapshot โ don't touch retention.
if [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; then
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6
else
echo "Backup failed with exit code $rc โ skipping forget." >&2
exit "$rc"
fi
Make it executable:
chmod +x ~/.config/restic/backup.sh
Note: this script runs forget (cheap metadata-only) but not prune. Pruning rewrites pack files and downloads data from B2 to do so โ running it daily is wasteful and can add meaningful egress cost on larger repos. Schedule pruning separately (see below).
Create the launchd plist ~/Library/LaunchAgents/com.restic.backup.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.restic.backup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>$HOME/.config/restic/backup.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>14</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/restic-backup.log</string>
<key>StandardErrorPath</key>
<string>/tmp/restic-backup.log</string>
</dict>
</plist>
Note: the plist parser itself doesn’t expand environment variables, which is why the script path is routed through bash -c โ bash expands $HOME at runtime. launchd sets HOME for LaunchAgents (user-scope), so this works without hardcoding your username.
Load it (modern macOS uses bootstrap/bootout; launchctl load is deprecated):
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.restic.backup.plist
To unload later: launchctl bootout gui/$(id -u)/com.restic.backup.
Create a separate weekly prune script ~/.config/restic/prune.sh:
#!/bin/bash
set -euo pipefail
# Same PATH fix as backup.sh โ launchd's default PATH doesn't include Homebrew.
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
# shellcheck source=/dev/null
source ~/.config/restic/env.sh
set +e
restic prune
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "Prune failed with exit code $rc." >&2
exit "$rc"
fi
chmod +x ~/.config/restic/prune.sh
Schedule it with a second LaunchAgent at ~/Library/LaunchAgents/com.restic.prune.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.restic.prune</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>$HOME/.config/restic/prune.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>4</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/restic-prune.log</string>
<key>StandardErrorPath</key>
<string>/tmp/restic-prune.log</string>
</dict>
</plist>
Note the separate log path (/tmp/restic-prune.log) so prune output doesn’t collide with backup output in the same file.
Load it the same way: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.restic.prune.plist.
3.7 Grant Full Disk Access
macOS sandboxes access to certain directories (~/Library/Mail, ~/Library/Messages, ~/Library/Safari, etc.) behind Full Disk Access (FDA). Without it, restic will silently skip those paths.
Grant FDA to two separate things โ interactive runs from your terminal and the automated launchd job use different execution paths, and FDA is granted per-binary:
- System Settings โ Privacy & Security โ Full Disk Access
- Click +, add your terminal app (Terminal, iTerm, etc.) โ this covers interactive runs.
- Click + again. You’ll need to grant FDA to the binary launchd actually runs under. The simplest option is to add
/bin/bashdirectly (pressCmd+Shift+Gin the file picker and enter/bin/bash), since the launch agent’sProgramArgumentsstarts with/bin/bash. If you prefer, add theresticbinary itself (/opt/homebrew/bin/resticon Apple Silicon,/usr/local/bin/resticon Intel) โ both approaches work. - You may need to restart Terminal for the interactive grant to take effect.
If you skip this step, the launchd job will run without errors but the resulting snapshots will be missing everything under the FDA-protected directories โ and you won’t notice until you need to restore your Mail or Messages.
3.8 Verify It’s Working
source ~/.config/restic/env.sh
restic snapshots
Linux
4.1 Install Restic
Debian/Ubuntu:
sudo apt update && sudo apt install restic
Fedora:
sudo dnf install restic
Arch:
sudo pacman -S restic
Manual download (any distro):
# Download latest release (check https://github.com/restic/restic/releases for current version)
curl -LO https://github.com/restic/restic/releases/download/v0.17.3/restic_0.17.3_linux_amd64.bz2
# Extract and install
bunzip2 restic_*.bz2
chmod +x restic_*
sudo mv restic_* /usr/local/bin/restic
4.2 Create Configuration
mkdir -p ~/.config/restic
Create ~/.config/restic/env.sh:
export RESTIC_REPOSITORY="s3:s3.us-west-004.backblazeb2.com/YOUR-BUCKET-NAME"
export RESTIC_PASSWORD="YOUR_SECURE_PASSWORD_HERE"
export AWS_ACCESS_KEY_ID="YOUR_B2_KEY_ID"
export AWS_SECRET_ACCESS_KEY="YOUR_B2_APPLICATION_KEY"
Secure the file:
chmod 600 ~/.config/restic/env.sh
Replace the placeholder values with your actual credentials.
4.3 Create Exclude File
Create ~/.config/restic/excludes.txt:
# System
.cache
.local/share/Trash
.thumbnails
*.tmp
*.temp
*~
# Browser caches (bookmarks/passwords ARE backed up)
.config/google-chrome/*/Cache
.config/google-chrome/*/Code Cache
.config/google-chrome/*/GPUCache
.config/BraveSoftware/Brave-Browser/*/Cache
.config/BraveSoftware/Brave-Browser/*/Code Cache
.mozilla/firefox/*/cache2
.mozilla/firefox/*/storage
# App caches
.config/Slack/Cache
.config/discord/Cache
.config/spotify/Data
.var/app/*/cache
# Development
node_modules
.venv
__pycache__
*.pyc
.npm
.cargo/registry
# Rust build output. This is scoped to `rust/target` because a bare `target`
# entry would match *any* directory named "target" anywhere in your home โ
# including e.g. Documents/target_audiences. Adjust the prefix to wherever
# you actually keep Rust code, or list specific project paths.
rust/target
# Large/replaceable
Downloads/*.iso
Downloads/*.AppImage
*.log
4.4 Initialise Repository
source ~/.config/restic/env.sh
restic init
Save the repository ID shown.
4.5 First Backup
Recommended: Exclude large files
Add --exclude-larger-than 300M to skip files over 300MB. Large files (videos, ISOs, game assets) are often replaceable and expensive to store in the cloud.
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --verbose
Know what this skips. A 300MB threshold silently excludes most 4K video clips, many stitched panoramas, large RAW panos, and photo library packages. If any of those are irreplaceable to you, raise the threshold or back them up separately โ the backup will otherwise finish cleanly without them and you won’t notice they’re missing until you need them.
Optional: Test with a dry run first
source ~/.config/restic/env.sh
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --dry-run -vv
This shows what would be backed up without uploading anything.
Run the actual backup:
source ~/.config/restic/env.sh
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --verbose
4.6 Automate with systemd
Create the backup script ~/.config/restic/backup.sh:
#!/bin/bash
set -euo pipefail
# shellcheck source=/dev/null
source ~/.config/restic/env.sh
# Run backup (skip files >300MB). Don't exit on non-zero here โ we want to
# inspect the code and only skip forget on a hard failure.
set +e
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M --quiet
rc=$?
set -e
# Only forget old snapshots if backup succeeded.
# Exit 0 = clean success, exit 3 = partial (some files unreadable, snapshot still created).
# Any other code means no usable snapshot โ don't touch retention.
if [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; then
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6
else
echo "Backup failed with exit code $rc โ skipping forget." >&2
exit "$rc"
fi
Make it executable:
chmod +x ~/.config/restic/backup.sh
Note: this script runs forget (cheap metadata-only) but not prune. Pruning rewrites pack files and downloads data from B2 to do so โ running it daily is wasteful and can add meaningful egress cost on larger repos. Schedule pruning separately (see below).
Create the systemd service ~/.config/systemd/user/restic-backup.service:
[Unit]
Description=Restic backup to Backblaze B2
[Service]
Type=oneshot
ExecStart=%h/.config/restic/backup.sh
Create the timer ~/.config/systemd/user/restic-backup.timer:
[Unit]
Description=Daily restic backup
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1800
[Install]
WantedBy=timers.target
Enable and start the timer:
mkdir -p ~/.config/systemd/user
# (copy the service and timer files above)
systemctl --user daemon-reload
systemctl --user enable --now restic-backup.timer
Notes:
Persistent=trueruns a missed backup when the computer next wakes (laptop-friendly)RandomizedDelaySec=1800staggers the backup within a 30-minute window (avoids thundering herd if you have multiple machines)%hexpands to your home directory in systemd unit files
Enable linger so the timer fires without an active login session:
sudo loginctl enable-linger "$USER"
By default, systemd --user services only run while you have an active graphical or SSH session. If you reboot and leave the machine at the login screen, or run this on a headless box you rarely log into interactively, the timer will silently not fire until someone logs in. enable-linger tells systemd to start your user manager at boot regardless, so scheduled backups actually happen. Run this once per machine.
Create a separate weekly prune job. Add the prune script ~/.config/restic/prune.sh:
#!/bin/bash
set -euo pipefail
# shellcheck source=/dev/null
source ~/.config/restic/env.sh
set +e
restic prune
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "Prune failed with exit code $rc." >&2
exit "$rc"
fi
chmod +x ~/.config/restic/prune.sh
Create ~/.config/systemd/user/restic-prune.service:
[Unit]
Description=Restic prune (reclaim space in B2 repository)
[Service]
Type=oneshot
ExecStart=%h/.config/restic/prune.sh
And ~/.config/systemd/user/restic-prune.timer:
[Unit]
Description=Weekly restic prune
[Timer]
OnCalendar=Sun 04:00
Persistent=true
RandomizedDelaySec=1800
[Install]
WantedBy=timers.target
Enable:
systemctl --user daemon-reload
systemctl --user enable --now restic-prune.timer
4.7 Verify It’s Working
# Check timer status
systemctl --user list-timers restic-backup.timer
# Check last run
journalctl --user -u restic-backup.service -n 20
# View snapshots
source ~/.config/restic/env.sh
restic snapshots
Restoring Files
List Your Backups
# macOS/Linux
source ~/.config/restic/env.sh
restic snapshots
# Windows PowerShell
. "$env:USERPROFILE\.config\restic\env.ps1"
restic snapshots
Restore a Specific File
# Find which snapshots contain your file
restic find "important-document.docx"
# Restore from latest backup
restic restore latest --target ~/restore-test --include "Documents/important-document.docx"
Restore Everything (Disaster Recovery)
On a fresh machine:
- Install restic
- Create env file with your credentials
- Restore to a staging directory, not to
/:
source ~/.config/restic/env.sh
mkdir -p ~/restore
restic restore latest --target ~/restore
Then inspect ~/restore/ and copy the files you want into their real locations. Restoring directly to / on a live system can clobber configuration, interact badly with running processes, and silently overwrite things you’d rather keep โ always stage the restore first.
Alternative: browse the repo as a filesystem. If you want to poke around before deciding what to pull back, restic mount ~/restore-mount exposes every snapshot as a read-only directory tree. Copy out what you need, then umount ~/restore-mount (or Ctrl-C).
Costs
| Data Size | Monthly Cost |
|---|---|
| 50 GB | ~$0.30 |
| 100 GB | ~$0.60 |
| 500 GB | ~$3.00 |
| 1 TB | ~$6.00 |
- Upload: Free
- Download: First 3ร your storage is free, then $0.01/GB
- API calls: Essentially free for personal use
Maintenance
Check Backup Health
Quick check (run monthly):
source ~/.config/restic/env.sh # or PowerShell equivalent
restic check
This validates the repository structure (snapshots, metadata, pack files).
Thorough check (run quarterly):
restic check --read-data
This downloads and verifies all backed-up data. Takes longer and uses bandwidth, but confirms your data is actually intact.
Partial check (compromise):
restic check --read-data-subset=10%
Randomly verifies 10% of your data โ useful for large repositories.
View Backup Statistics
restic stats
restic stats latest
Manual Backup
Run anytime you want an immediate backup:
source ~/.config/restic/env.sh
restic backup ~ --exclude-file="$HOME/.config/restic/excludes.txt" --exclude-larger-than 300M
Manual Prune
The automated setup above runs prune weekly. Run it on demand if you’ve just deleted a large amount of data and want to reclaim the space in B2 immediately, rather than waiting for the next scheduled run:
source ~/.config/restic/env.sh
# Preview which snapshots would be forgotten (recommended first)
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run
# Actually reclaim space
restic prune
# Verify repository integrity after pruning
restic check
Optional: Automatic Git Commits for Your Notes
If you use Obsidian, Logseq, or any markdown-based notes system, git provides fine-grained version history that restic’s daily snapshots can’t match. Accidentally delete a paragraph? Git has it from 10 minutes ago.
Why Git + Restic?
They solve different problems:
- Git: Tracks every change to text files, lets you see exactly what changed and when
- Restic: Backs up everything (including large files git ignores) to the cloud
Use both.
Windows: Git Setup
Install git:
winget install --id Git.Git -e --source winget
Initialise your vault:
cd "$env:USERPROFILE\Documents\Obsidian Vault" # Your vault path
git init
git config user.email "autobackup@local"
git config user.name "Auto-Backup"
Create .gitignore (track only markdown):
# Ignore everything
*
# But track markdown
!*.md
# Track Obsidian config
!.obsidian/
!.obsidian/**
# Track folder structure
!*/
# Track this file
!.gitignore
# Ignore workspace (local state)
.obsidian/workspace*.json
# Ignore trash
.trash/
Create commit script %USERPROFILE%\.config\restic\git-commit.ps1:
$vaultPath = "$env:USERPROFILE\Documents\Obsidian Vault" # Your vault path
$logFile = "$env:USERPROFILE\.config\restic\git-commit.log"
# Guard: bail out if vault path doesn't exist
if (-not (Test-Path $vaultPath)) {
"$(Get-Date -Format 'yyyy-MM-dd HH:mm') ERROR: Vault path not found: $vaultPath" |
Out-File -Append $logFile
exit 1
}
Set-Location $vaultPath
# Auto-repair corrupted git objects (e.g. from crash mid-write)
$emptyObjects = Get-ChildItem -Path .git\objects -Recurse -File | Where-Object { $_.Length -eq 0 }
if ($emptyObjects) {
"$(Get-Date -Format 'yyyy-MM-dd HH:mm') Found empty git objects, repairing..." |
Out-File -Append $logFile
$emptyObjects | Remove-Item -Force
$fsck = git fsck --no-dangling --quiet 2>&1
if ($LASTEXITCODE -ne 0) {
$branch = git symbolic-ref --short HEAD 2>$null
if (-not $branch) { $branch = "main" }
$reflog = ".git\logs\refs\heads\$branch"
if (Test-Path $reflog) {
$lastGood = $null
foreach ($line in Get-Content $reflog) {
$old = ($line -split ' ')[0]
$check = git cat-file -t $old 2>$null
if ($LASTEXITCODE -eq 0) { $lastGood = $old }
}
if ($lastGood) {
git update-ref "refs/heads/$branch" $lastGood
"$(Get-Date -Format 'yyyy-MM-dd HH:mm') Reset $branch to $lastGood" |
Out-File -Append $logFile
} else {
"$(Get-Date -Format 'yyyy-MM-dd HH:mm') ERROR: No valid commit to recover to" |
Out-File -Append $logFile
exit 1
}
} else {
"$(Get-Date -Format 'yyyy-MM-dd HH:mm') ERROR: No reflog for $branch" |
Out-File -Append $logFile
exit 1
}
}
}
# Check if there are changes
$status = git status --porcelain
if (-not $status) {
exit 0
}
# Commit all changes
git add -A
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm"
git commit -m "Auto-backup: $timestamp"
"$timestamp Committed" | Out-File -Append $logFile
Schedule every 10 minutes: Create a Task Scheduler task using the same process as the backup task but with:
- Program:
powershell.exe - Arguments:
-ExecutionPolicy Bypass -File "%USERPROFILE%\.config\restic\git-commit.ps1" - Trigger: Daily, repeat every 10 minutes
macOS: Git Setup
Install git (usually pre-installed, or):
xcode-select --install
Initialise your vault:
cd ~/Documents/Obsidian\ Vault # Your vault path
git init
git config user.email "autobackup@local"
git config user.name "Auto-Backup"
Create .gitignore (same content as Windows above).
Create commit script ~/.config/restic/git-commit.sh:
#!/bin/bash
set -u
# launchd's default PATH doesn't include Homebrew. If you installed git via
# `brew install git` on Apple Silicon, this export is required for the
# scheduled run to find it. (Xcode git at /usr/bin/git is in PATH by default.)
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
VAULT_PATH="$HOME/Documents/Obsidian Vault" # Your vault path
LOG_FILE="$HOME/.config/restic/git-commit.log"
cd "$VAULT_PATH" || { echo "$(date '+%Y-%m-%d %H:%M') ERROR: Vault path not found: $VAULT_PATH" >> "$LOG_FILE"; exit 1; }
# Auto-repair corrupted git objects (e.g. from crash mid-write)
empty_objects=$(find .git/objects/ -type f -empty 2>/dev/null)
if [[ -n "$empty_objects" ]]; then
echo "$(date '+%Y-%m-%d %H:%M') Found empty git objects, repairing..." >> "$LOG_FILE"
while IFS= read -r f; do rm -f "$f"; done <<< "$empty_objects"
if ! git fsck --no-dangling --quiet >/dev/null 2>&1; then
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "main")
reflog=".git/logs/refs/heads/$branch"
if [[ -f "$reflog" ]]; then
last_good=""
while IFS=' ' read -r old new _rest; do
if git cat-file -t "$old" >/dev/null 2>&1; then
last_good="$old"
fi
done < "$reflog"
if [[ -n "$last_good" ]]; then
git update-ref "refs/heads/$branch" "$last_good"
echo "$(date '+%Y-%m-%d %H:%M') Reset $branch to $last_good" >> "$LOG_FILE"
else
echo "$(date '+%Y-%m-%d %H:%M') ERROR: No valid commit to recover to" >> "$LOG_FILE"
exit 1
fi
else
echo "$(date '+%Y-%m-%d %H:%M') ERROR: No reflog for $branch" >> "$LOG_FILE"
exit 1
fi
fi
fi
# Check if there are changes
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
exit 0
fi
# Commit all changes
git add -A
TIMESTAMP=$(date "+%Y-%m-%d %H:%M")
git commit -m "Auto-backup: $TIMESTAMP"
echo "$TIMESTAMP Committed" >> "$LOG_FILE"
Make executable:
chmod +x ~/.config/restic/git-commit.sh
Schedule every 10 minutes: Create ~/Library/LaunchAgents/com.git.autocommit.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.git.autocommit</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>$HOME/.config/restic/git-commit.sh</string>
</array>
<key>StartInterval</key>
<integer>600</integer>
</dict>
</plist>
Load it (launchctl load is deprecated; use bootstrap):
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.git.autocommit.plist
Linux: Git Setup
Install git (usually pre-installed, or):
# Debian/Ubuntu
sudo apt install git
# Fedora
sudo dnf install git
Initialise your vault:
cd ~/Documents/Obsidian\ Vault # Your vault path
git init
git config user.email "autobackup@local"
git config user.name "Auto-Backup"
Create .gitignore (same content as Windows/macOS above).
Create commit script ~/.config/restic/git-commit.sh:
#!/bin/bash
set -u
VAULT_PATH="$HOME/Documents/Obsidian Vault" # Your vault path
LOG_FILE="$HOME/.config/restic/git-commit.log"
cd "$VAULT_PATH" || { echo "$(date '+%Y-%m-%d %H:%M') ERROR: Vault path not found: $VAULT_PATH" >> "$LOG_FILE"; exit 1; }
# Auto-repair corrupted git objects (e.g. from crash mid-write)
empty_objects=$(find .git/objects/ -type f -empty 2>/dev/null)
if [[ -n "$empty_objects" ]]; then
echo "$(date '+%Y-%m-%d %H:%M') Found empty git objects, repairing..." >> "$LOG_FILE"
while IFS= read -r f; do rm -f "$f"; done <<< "$empty_objects"
if ! git fsck --no-dangling --quiet >/dev/null 2>&1; then
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "main")
reflog=".git/logs/refs/heads/$branch"
if [[ -f "$reflog" ]]; then
last_good=""
while IFS=' ' read -r old new _rest; do
if git cat-file -t "$old" >/dev/null 2>&1; then
last_good="$old"
fi
done < "$reflog"
if [[ -n "$last_good" ]]; then
git update-ref "refs/heads/$branch" "$last_good"
echo "$(date '+%Y-%m-%d %H:%M') Reset $branch to $last_good" >> "$LOG_FILE"
else
echo "$(date '+%Y-%m-%d %H:%M') ERROR: No valid commit to recover to" >> "$LOG_FILE"
exit 1
fi
else
echo "$(date '+%Y-%m-%d %H:%M') ERROR: No reflog for $branch" >> "$LOG_FILE"
exit 1
fi
fi
fi
# Check if there are changes
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
exit 0
fi
# Commit all changes
git add -A
TIMESTAMP=$(date "+%Y-%m-%d %H:%M")
git commit -m "Auto-backup: $TIMESTAMP"
echo "$TIMESTAMP Committed" >> "$LOG_FILE"
Make executable:
chmod +x ~/.config/restic/git-commit.sh
Schedule every 10 minutes with systemd:
Create ~/.config/systemd/user/git-autocommit.service:
[Unit]
Description=Auto-commit git changes
[Service]
Type=oneshot
ExecStart=%h/.config/restic/git-commit.sh
Create ~/.config/systemd/user/git-autocommit.timer:
[Unit]
Description=Auto-commit git changes every 10 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=10min
[Install]
WantedBy=timers.target
Enable:
systemctl --user daemon-reload
systemctl --user enable --now git-autocommit.timer
Recovering from Git
# See recent commits
git log --oneline -20
# See what changed in a commit
git show abc1234
# Restore a deleted file
git checkout HEAD~1 -- "path/to/file.md"
# See file at a specific time
git show HEAD~5:"path/to/file.md"
Troubleshooting
Understanding Exit Codes
Restic uses exit codes to indicate what happened:
- 0: Success โ backup completed normally
- 1: Fatal error โ no snapshot created
- 3: Partial success โ some files couldn’t be read, but snapshot was created
Exit code 3 is common if some files are locked by other applications. The backup still succeeds for everything else.
“repository does not exist”
Check your RESTIC_REPOSITORY path. The format is:
s3:s3.REGION.backblazeb2.com/BUCKET-NAME
“invalid credentials”
Verify AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY match your B2 application key exactly.
Backup is slow
First backup uploads everything โ this is normal. Subsequent backups only upload changes and should be much faster.
To limit bandwidth during work hours, add --limit-upload 5000 (5 MB/s) to your backup command.
Task Scheduler / launchd not running
Windows: Check Task Scheduler โ Task History. Common issues:
- PowerShell execution policy blocking the script
- Wrong path to script
macOS: Check /tmp/restic-backup.log (and /tmp/restic-prune.log) for errors. Common issues:
restic: command not foundโ launchd’s PATH doesn’t include Homebrew. Theexport PATH=...line inbackup.shshould fix this; verify it’s present.- Missing files in snapshots โ Full Disk Access not granted to the binary launchd runs under. See ยง3.7.
operation not permittedon~/Library/Mailor similar โ same FDA issue.
Key Points
- Save your password. If you lose it, your backups are unrecoverable. Put it in your password manager.
- Test restores. Periodically restore a file to verify your backups work.
- First backup takes time. Let it run overnight if needed.
- This complements sync services. Use OneDrive/iCloud for convenience, restic for disaster recovery.