DEV Community

Luna Commsnet
Luna Commsnet

Posted on

Automated Homelab Backups: Borg and Restic for Disaster Recovery

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
Enter fullscreen mode Exit fullscreen mode

On your backup server (the target):

sudo apt update
sudo apt install -y borgbackup
Enter fullscreen mode Exit fullscreen mode

💡 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
Enter fullscreen mode Exit fullscreen mode

⚠️ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

Create the passphrase file:

echo "your-super-secure-passphrase" | sudo tee /etc/borgmatic.d/passphrase
sudo chmod 600 /etc/borgmatic.d/passphrase
Enter fullscreen mode Exit fullscreen mode

Test the config:

sudo borgmatic create --verbosity 1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

5.2 Schedule VM Backups in Proxmox GUI

  1. Navigate to Datacenter → Backup → Add
  2. Set:
    • Storage: local-btrfs (or your backup storage)
    • Schedule: 02:00 daily
    • Selection: All VMs (or specific IDs)
    • Mode: Snapshot
    • Compression: Zstd
  3. 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/*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# /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
Enter fullscreen mode Exit fullscreen mode
sudo systemctl enable --now borg-backup.timer
systemctl list-timers borg-backup.timer
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or in borgmatic:

retention:
  keep_daily: 7
  keep_weekly: 4
  keep_monthly: 6
  keep_yearly: 1
  prefix: "{hostname}-"  # Only prune archives from this host
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡 Tip: Run restic prune weekly, 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)
Enter fullscreen mode Exit fullscreen mode

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

  1. Automate everything — borgmatic + systemd timers + resticprofile = hands-off backups
  2. Monitor aggressively — Zabbix + ntfy/Telegram notifications mean you know about failures immediately
  3. Test restores regularly — mount a backup, verify files, document the process
  4. Use retention policies — GFS keeps a year of recovery points without consuming infinite storage
  5. Defense in depth — Borg for local (fast restores), Restic for offsite (survives site loss)
  6. 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
Enter fullscreen mode Exit fullscreen mode

Useful Resources

Sleep well knowing your homelab is protected! 🛡️

Top comments (0)