Automated Homelab Backups: Borg and Restic for Disaster Recovery
1. Introduction — Why Automated Backups Matter for Homelabs
Your homelab is running 15 Docker containers, three VMs, a Nextcloud instance with family photos, and a Gitea server with two years of code commits. When was the last time you tested a restore?
If you're like most homelab operators, the answer is "never" or "I think it worked when I set it up." That's not a backup — that's a prayer.
What Makes a Good Backup Strategy?
The 3-2-1 rule, established by photographer Peter Krogh, remains the gold standard:
- 3 copies of your data
- 2 different storage media (e.g., local NAS + offsite VPS)
- 1 copy offsite (cloud, VPS, or friend's server)
A good backup system is:
- Automated — if it requires manual steps, you'll skip it
- Monitored — silent failures are worse than no backup
- Tested — an untested backup is a hypothesis, not a guarantee
- Versioned — you need to restore from last Tuesday, not just "the latest"
Borg vs Restic: Which One?
Both are excellent, modern, deduplicating backup tools. Here's how to choose:
| Feature | Borg | Restic |
|---|---|---|
| Language | Python (C extensions) | Go |
| Repository format | Custom | Custom |
| Deduplication | Chunk-level, excellent | Chunk-level, excellent |
| Compression | lz4, zlib, zstd | (none built-in, use external) |
| Encryption | Optional (repo-level) | Mandatory (always encrypted) |
| Backends | Local, SSH/SFTP | Local, SFTP, S3, Backblaze, REST server |
| Web UI | Borgmatic, BorgWarehouse | Restic Profile, Restic Browser |
| Mount backup | Yes (FUSE) | Yes (FUSE) |
| Maturity | 2010, very mature | 2014, mature |
| Best for | Local + SSH backups | Cloud/S3 backups |
| Config format | YAML (via borgmatic) | TOML (via resticprofile) |
My recommendation for homelabs:
- Use Borg for local/NAS backups (better compression, mature tooling)
- Use Restic for offsite/S3 backups (native cloud backend support)
- Run both in parallel for defense in depth
2. Prerequisites
| Item | Purpose |
|---|---|
| Proxmox VE host | The thing you're backing up |
| Backup server | Local NAS, dedicated backup machine, or VPS ($3-5/mo) |
| SSH access | Between Proxmox host and backup server |
| Root/sudo | On both Proxmox and backup server |
| Storage space | At least 2x your data size (dedup helps, but first backup is full) |
Storage Planning
| Data Type | Size | Dedup Ratio | Backup Space Needed |
|---|---|---|---|
| VM disk images | 50-200GB each | ~1.5x (sparse files) | 100-300GB |
| Nextcloud files | 10-100GB | ~1.2x (already compressed) | 20-120GB |
| Docker volumes | 5-50GB | ~2x (good dedup) | 5-25GB |
| Home Assistant config | <1GB | ~3x (tiny, dedup-friendly) | <1GB |
| Gitea repos | 1-10GB | ~2x | 1-5GB |
💡 Tip: Borg's deduplication is excellent across similar VMs. If you back up 5 Ubuntu VMs that share the same base packages, dedup can reduce total backup size by 60-80%.
3. Borg Backup Setup
3.1 Installing Borg
On your Proxmox host (the source):
sudo apt update
sudo apt install -y borgbackup borgmatic
On your backup server (the target):
sudo apt update
sudo apt install -y borgbackup
💡 Tip: Borg 1.2+ is recommended. Check your version with
borg --version. If your distro ships an older version, install from the official Borg repo or use the static binary from https://github.com/borgbackup/borg/releases.
3.2 Initialize a Borg Repository
On your Proxmox host, create a repository on the backup server:
# Create a directory on the backup server (via SSH)
ssh backupuser@backup-server mkdir -p /backups/proxmox
# Initialize the Borg repository with encryption
# You'll be prompted to set a repository password — SAVE THIS SECURELY
borg init --encryption repokey-blake2 backupuser@backup-server:/backups/proxmox
# Verify the repository
borg info backupuser@backup-server:/backups/proxmox
⚠️ Warning: If you lose the repository password, your backups are unrecoverable. Store it in a password manager (Bitwarden, KeePassXC) and keep an offline copy in a safe.
3.3 Creating Your First Backup
# Create a backup of /etc and /var/lib/docker
borg create \
--stats \
--progress \
--compression zstd,3 \
backupuser@backup-server:/backups/proxmox::"{hostname}-{now}" \
/etc \
/var/lib/docker \
/var/lib/libvirt \
/root
# The archive name uses {hostname}-{now} which expands to:
# proxmox-2026-07-26T01:46:00
Understanding the command:
-
--stats— show dedup/compression statistics after completion -
--progress— show a progress bar during backup -
--compression zstd,3— use Zstandard compression level 3 (good balance of speed/ratio) -
::"{hostname}-{now}"— archive name template (hostname + timestamp) - The paths at the end are what gets backed up
Output example:
Archive name: proxmox-2026-07-26T01:46:00
Archive fingerprint: a1b2c3d4e5...
Time (start): Sat Jul 26 01:46:00 2026
Time (end): Sat Jul 26 01:48:32 2026
Duration: 2 minutes 32 seconds
Number of files: 45283
Original size: 12.5 GB
Deduplicated size: 4.2 GB (66% reduction)
Compressed size: 3.1 GB
3.4 Borgmatic Configuration for Automation
Borgmatic is a YAML-based wrapper around Borg that makes automation much cleaner:
# /etc/borgmatic.d/proxmox.yaml
location:
repositories:
- path: backupuser@backup-server:/backups/proxmox
label: nas
source_directories:
- /etc
- /var/lib/docker
- /var/lib/libvirt
- /root
- /home
exclude_patterns:
- /var/lib/docker/overlay2/*/diff/tmp/*
- /var/lib/libvirt/images/*.iso
- **/__pycache__
- **/node_modules
- **/.cache
storage:
compression: zstd,3
encryption_passcommand: "cat /etc/borgmatic.d/passphrase"
ssh_command: ssh -i /root/.ssh/backup_key
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 1
hooks:
before_backup:
- echo "Starting backup at $(date)"
after_backup:
- echo "Backup completed at $(date)"
on_error:
- curl -s -X POST https://ntfy.commsnet.org/backup-error \
-d "Borg backup FAILED on $(hostname) at $(date)"
Create the passphrase file:
echo "your-super-secure-passphrase" | sudo tee /etc/borgmatic.d/passphrase
sudo chmod 600 /etc/borgmatic.d/passphrase
Test the config:
sudo borgmatic create --verbosity 1
4. Restic Setup
4.1 Installing Restic
On your Proxmox host:
# Debian/Ubuntu
sudo apt update
sudo apt install -y restic
# Or install the latest binary directly
wget https://github.com/restic/restic/releases/download/v0.16.4/restic_0.16.4_linux_amd64.bz2
bunzip2 restic_0.16.4_linux_amd64.bz2
sudo mv restic_0.16.4_linux_amd64 /usr/local/bin/restic
sudo chmod +x /usr/local/bin/restic
4.2 Configuring a Repository
Restic supports multiple backends. Here are the most useful for homelabs:
Local or SFTP (SSH):
# Local directory (e.g., mounted NAS)
export RESTIC_REPOSITORY="/mnt/nas/backups/proxmox-restic"
export RESTIC_PASSWORD_FILE="/etc/restic/passphrase"
# Or SFTP (remote server via SSH)
export RESTIC_REPOSITORY="sftp:backupuser@backup-server:/backups/proxmox-restic"
export RESTIC_PASSWORD_FILE="/etc/restic/passphrase"
restic init
S3-compatible (MinIO, Backblaze B2, Cloudflare R2):
export RESTIC_REPOSITORY="s3:https://s3.backblazeb2.com/commsnet-backups"
export RESTIC_PASSWORD_FILE="/etc/restic/passphrase"
export AWS_ACCESS_KEY_ID="your-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
restic init
💡 Tip: Backblaze B2 offers 10GB free storage — enough for small homelab configs and documents. For larger backups, their pricing ($0.005/GB/month) is very competitive.
4.3 Creating Snapshots
# Create a snapshot
restic backup /etc /var/lib/docker /var/lib/libvirt /root \
--tag proxmox \
--tag daily \
--verbose
# List snapshots
restic snapshots
# Show snapshot details
restic stats
4.4 Restic Profiles for Automation
For cleaner automation, use resticprofile — a config-driven wrapper:
# /etc/resticprofile/proxmox.conf
[default]
repository = "sftp:backupuser@backup-server:/backups/proxmox-restic"
password_file = "/etc/restic/passphrase"
[default.backup]
source = ["/etc", "/var/lib/docker", "/var/lib/libvirt", "/root"]
exclude = ["/var/lib/docker/overlay2/*/diff/tmp/*", "**/__pycache__", "**/node_modules"]
tag = ["proxmox", "daily"]
schedule = "daily"
schedule_permision = "system"
[default.retention]
keep_daily = 7
keep_weekly = 4
keep_monthly = 6
keep_yearly = 1
[default.check]
schedule = "weekly"
Install resticprofile and enable:
# Install resticprofile
wget https://github.com/creativeprojects/resticprofile/releases/download/v0.17.0/resticprofile_0.17.0_linux_amd64.tar.gz
tar xzf resticprofile_0.17.0_linux_amd64.tar.gz
sudo mv resticprofile /usr/local/bin/
# Schedule with systemd
sudo resticprofile schedule
5. Backing Up Proxmox VMs
Proxmox has its own backup system (vzdump), but it stores backups locally by default. Here's how to combine it with Borg/Restic for offsite copies:
5.1 Configure vzdump for Local Backups
# /etc/vzdump.conf
dumpdir: /var/lib/vz/dump
storage: local
compress: zstd
mode: snapshot # Use snapshot mode for running VMs (requires LVM/thin)
5.2 Schedule VM Backups in Proxmox GUI
- Navigate to Datacenter → Backup → Add
- Set:
- Storage: local-btrfs (or your backup storage)
- Schedule: 02:00 daily
- Selection: All VMs (or specific IDs)
- Mode: Snapshot
- Compression: Zstd
- Save
5.3 Offsite the vzdump Backups with Borg
Add the vzdump backup directory to your borgmatic config:
# Add to /etc/borgmatic.d/proxmox.yaml source_directories:
source_directories:
- /etc
- /var/lib/docker
- /var/lib/libvirt
- /var/lib/vz/dump # Proxmox VM backups
# Exclude large ISO images
exclude_patterns:
- /var/lib/vz/dump/*.iso
- /var/lib/vz/template/iso/*
5.4 Alternative: Hook Borg into vzdump
Create a hook script that runs Borg after vzdump completes:
#!/bin/bash
# /usr/local/bin/vzdump-hook-borg.sh
# Called by vzdump with $1=phase, $2=vmid
phase=$1
vmid=$2
if [ "$phase" = "job-end" ]; then
# Run Borg backup of the vzdump directory after all VMs are dumped
/usr/bin/borgmatic create --stats --progress 2>&1 | logger -t borgmatic
# Also run restic for offsite
/usr/bin/restic backup /var/lib/vz/dump --tag proxmox-vm --tag vzdump 2>&1 | logger -t restic
fi
# Make it executable
sudo chmod +x /usr/local/bin/vzdump-hook-borg.sh
# Add to vzdump job (via Proxmox GUI or CLI):
# vzdump --hookscript /usr/local/bin/vzdump-hook-borg.sh
6. Backing Up Critical Services
6.1 Nextcloud Data
# Nextcloud maintenance mode (ensures consistent backup)
sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --on
# Borg backup of Nextcloud data and config
borg create \
--compression zstd,3 \
backupuser@backup-server:/backups/nextcloud::"{now}" \
/var/www/nextcloud/config \
/var/www/nextcloud/data \
/var/www/nextcloud/apps
# Turn off maintenance mode
sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --off
Add to borgmatic with pre/post hooks:
hooks:
before_backup:
- sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --on
after_backup:
- sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --off
on_error:
- sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --off
6.2 Home Assistant Configuration
# Home Assistant has a built-in backup mechanism
# Trigger it via API before backing up
curl -X POST \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
http://homeassistant:8123/api/services/backup/generate
# Then backup the snapshots directory
borg create \
backupuser@backup-server:/backups/ha::"{now}" \
/config/backups \
/config/configuration.yaml \
/config/automations.yaml
6.3 Docker Volumes
# Stop containers for consistent backup, or use --ignore-fs-type
# Option A: Stop, backup, start (safest)
docker compose stop
borg create backupuser@backup-server:/backups/docker::"{now}" /var/lib/docker/volumes
docker compose start
# Option B: Use Borg's --ignore-fs-type to skip overlay filesystems
# (backs up bind mounts but skips Docker's internal storage)
borg create \
--exclude-if-present .nobackup \
backupuser@backup-server:/backups/docker::"{now}" \
/opt/containers # Your compose project directories with bind mounts
6.4 Gitea Repositories
# Use Gitea's built-in dump command for a consistent backup
sudo -u git gitea dump --config /etc/gitea/app.ini --file /tmp/gitea-dump.zip
# Then Borg the dump
borg create \
backupuser@backup-server:/backups/gitea::"{now}" \
/tmp/gitea-dump.zip \
/etc/gitea
# Clean up
rm /tmp/gitea-dump.zip
7. Automation with Cron/Systemd Timers
7.1 Systemd Timer (Recommended)
# /etc/systemd/system/borg-backup.service
[Unit]
Description=Borg Backup Service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/borgmatic create --stats
ExecStart=/usr/bin/borgmatic prune --stats
User=root
# /etc/systemd/system/borg-backup.timer
[Unit]
Description=Daily Borg Backup
[Timer]
OnCalendar=daily
Persistent=true # Run immediately if a scheduled run was missed
RandomizedDelaySec=300 # Add up to 5 minutes of jitter
[Install]
WantedBy=timers.target
sudo systemctl enable --now borg-backup.timer
systemctl list-timers borg-backup.timer
7.2 Cron (Alternative)
# /etc/cron.d/borg-backup
# Run Borg daily at 2:00 AM
0 2 * * * root /usr/bin/borgmatic create --stats 2>&1 | logger -t borgmatic
# Prune old backups weekly
0 3 * * 0 root /usr/bin/borgmatic prune --stats 2>&1 | logger -t borgmatic
# Verify repository integrity monthly
0 4 1 * * root /usr/bin/borgmatic check --progress 2>&1 | logger -t borgmatic
8. Monitoring Backups
8.1 Zabbix Integration
Create a Zabbix user parameter to monitor backup status:
# /etc/zabbix/zabbix_agentd.d/borgbackup.conf
UserParameter=borg.last_backup_age,expr $(( $(date +%s) - $(stat -c %Y /var/log/borgmatic/last_success) ))
UserParameter=borg.backup_status,cat /var/log/borgmatic/last_status 2>/dev/null || echo "unknown"
UserParameter=borg.repo_size,borg info backupuser@backup-server:/backups/proxmox 2>/dev/null | grep "Deduplicated size" | tail -1 | awk '{print $3}'
Add a Zabbix trigger that fires if:
-
borg.last_backup_age> 86400 (24 hours since last backup) -
borg.backup_status!= "success"
8.2 Ntfy/Telegram Notifications
Borgmatic ntfy hook:
hooks:
after_backup:
- curl -s -X POST https://ntfy.commsnet.org/backups \
-d "✅ Borg backup completed on $(hostname) — $(date)"
on_error:
- curl -s -X POST https://ntfy.commsnet.org/backups \
-H "Priority: high" \
-d "❌ Borg backup FAILED on $(hostname) — $(date)"
Restic Telegram notification:
#!/bin/bash
# /usr/local/bin/restic-notify.sh
CHAT_ID="your-chat-id"
BOT_TOKEN="your-bot-token"
if [ $? -eq 0 ]; then
MESSAGE="✅ Restic backup completed on $(hostname) at $(date)"
else
MESSAGE="❌ Restic backup FAILED on $(hostname) at $(date)"
fi
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" -d "text=${MESSAGE}"
9. Testing Restores
⚠️ The most important section of this entire article. An untested backup is NOT a backup.
9.1 Borg Restore Test
# List available archives
borg list backupuser@backup-server:/backups/proxmox
# Mount an archive as a filesystem (read-only)
mkdir -p /mnt/restore-test
borg mount backupuser@backup-server:/backups/proxmox /mnt/restore-test
# Browse and verify files
ls /mnt/restore-test/proxmox-2026-07-26T01:46:00/etc/
diff /mnt/restore-test/proxmox-2026-07-26T01:46:00/etc/hosts /etc/hosts
# Unmount when done
borg umount /mnt/restore-test
9.2 Restic Restore Test
# List snapshots
restic snapshots
# Restore a specific snapshot to a temp directory
restic restore latest --target /tmp/restore-test --include /etc
# Verify a specific file
diff /tmp/restore-test/etc/hosts /etc/hosts
# Or mount and browse
mkdir -p /mnt/restic-mount
restic mount /mnt/restic-mount
# Browse to /mnt/restic-mount/snapshots/latest/
9.3 Automated Restore Testing
Create a script that tests restores monthly:
#!/bin/bash
# /usr/local/bin/test-restore.sh
TEST_DIR="/tmp/restore-test-$(date +%s)"
mkdir -p "$TEST_DIR"
# Restore /etc as a canary
borg extract \
backupuser@backup-server:/backups/proxmox::proxmox-$(date +%Y-%m-%d) \
etc/hosts etc/hostname etc/passwd
# Verify critical files exist
for file in etc/hosts etc/hostname etc/passwd; do
if [ ! -f "$file" ]; then
echo "FAIL: $file not found in restore"
exit 1
fi
done
echo "Restore test passed"
rm -rf "$TEST_DIR"
Schedule it: 0 5 1 * * root /usr/local/bin/test-restore.sh 2>&1 | logger -t restore-test
10. Retention Policies
10.1 Grandfather-Father-Son (GFS)
The classic GFS rotation keeps backups at multiple granularities:
| Period | Keep | What it means |
|---|---|---|
| Daily | 7 | One backup per day, keep last 7 |
| Weekly | 4 | One backup per week, keep last 4 |
| Monthly | 6 | One backup per month, keep last 6 |
| Yearly | 1 | One backup per year, keep 1 |
This gives you: 7 daily + 4 weekly + 6 monthly + 1 yearly = 18 archives total, covering an entire year of recovery points.
10.2 Borg Prune
# Prune old archives according to retention policy
borg prune \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 1 \
backupuser@backup-server:/backups/proxmox
Or in borgmatic:
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 1
prefix: "{hostname}-" # Only prune archives from this host
10.3 Restic Forget + Prune
# Mark old snapshots for deletion
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 1
# Actually delete the data (frees space)
restic prune
# Or combine with backup command
restic backup /etc --tag daily
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --keep-yearly 1 --prune
💡 Tip: Run
restic pruneweekly, not daily. Pruning reorganizes the repository and can be slow on large repos. Forgetting just marks snapshots; pruning actually removes the data.
10.4 Repository Check
Periodically verify your repository integrity:
# Borg check (monthly)
borg check --verify-data backupuser@backup-server:/backups/proxmox
# Restic check (weekly for metadata, monthly for full data)
restic check # Quick metadata check
restic check --read-data # Full data integrity check (slower)
11. Conclusion — Summary and Key Takeaways
You now have a robust, automated, monitored, tested backup system. Here's the complete picture:
Your Backup Stack
| Layer | Tool | Frequency | Target | Retention |
|---|---|---|---|---|
| VM-level | Proxmox vzdump | Daily 2AM | Local storage | 7 daily |
| File-level | Borg | Daily 2:30AM | NAS via SSH | 7d/4w/6m/1y |
| Offsite | Restic | Daily 3AM | S3/B2 | 7d/4w/6m/1y |
| Integrity | Borg check | Monthly | — | — |
| Restore test | Automated script | Monthly | /tmp | — |
Key Takeaways
- Automate everything — borgmatic + systemd timers + resticprofile = hands-off backups
- Monitor aggressively — Zabbix + ntfy/Telegram notifications mean you know about failures immediately
- Test restores regularly — mount a backup, verify files, document the process
- Use retention policies — GFS keeps a year of recovery points without consuming infinite storage
- Defense in depth — Borg for local (fast restores), Restic for offsite (survives site loss)
- Document your restore process — when disaster strikes at 3 AM, you don't want to be reading man pages
Restore Runbook Template
1. SSH to backup server: ssh backupuser@backup-server
2. List archives: borg list /backups/proxmox
3. Mount archive: borg mount /backups/proxmox::proxmox-2026-07-26 /mnt/restore
4. Copy files: rsync -av /mnt/restore/etc/ /etc/
5. Verify services: systemctl status docker && docker compose ps
6. Document what was restored and why
Useful Resources
Sleep well knowing your homelab is protected! 🛡️
Top comments (0)