Blog
PLEN

Restic + Backblaze B2 w homelabie — backup który realnie działa, kosztuje 5 zł/mc i daje spać

Po dwóch latach z różnymi rozwiązaniami (Borg, Duplicati, ręczne rsync) wylądowałem na Restic + B2. Pełen setup z systemd timer, encryption, retention policy i monitoringiem do Telegrama.

·5 min read
Restic + Backblaze B2 w homelabie — backup który realnie działa, kosztuje 5 zł/mc i daje spać

Pierwszy raz „prawdziwy" backup zrobiłem po stracie Proxmox VM z databasie betting-hope w 2024. SSD się rozsypał w środku tygodnia, ostatni snapshot miał 3 dni, czyli straciłem 3 dni danych. Od tamtej pory mam pewną zasadę: dane bez backupu to dane na czas określony.

Dziś u mnie Restic + Backblaze B2 obsługuje backupy 4 maszyn, 6 baz danych, ~80 GB danych. Koszt: 5 zł/mc. Wiarygodność: testowane restore co miesiąc, działa.

To jest dokładny setup, copy-paste, nie filozofia.

Dlaczego Restic, a nie Borg / Duplicati / Restic Browser

Krótko:

  • Borg, świetny, ale chunked storage trzyma dane w binary format trudnym do recovery jak utracisz binarkę. Restic ma open spec.
  • Duplicati, UI ładny, ale crashe przy dużych repo, słaby support polish backupów.
  • Restic, Go single binary, encryption AES-256 + Poly1305, dedup, snapshots, wsparcie 15+ backendów (B2, S3, Azure, GCS, SFTP, REST itp.). Stable od 2017.
  • Co odpada od razu: gołe rsync (brak dedup, brak encryption), tar.gz (brak incremental).

Architektura — co backup, gdzie, jak często

┌─────────────────────────────────────────┐
│ Główna maszyna (homelab core)           │
│ - /home/kkaletka (configs, projekty)    │
│ - /var/lib/docker/volumes (DB volumes)  │
│ - /etc (system config)                  │
│ - PostgreSQL dump (każde 6h)            │
└─────────────────────────────────────────┘

                  │  restic backup --tag $(hostname)

┌─────────────────────────────────────────┐
│ Backblaze B2 bucket: kkaletka-backup   │
│ region: eu-central                      │
│ encryption: server-side + client-side   │
│ retention: 7d daily + 4w weekly + 12m  │
└─────────────────────────────────────────┘

Cztery maszyny push'ują do tego samego bucketu, każda ma własny tag. Restore selektywny per host.

Setup, krok po kroku

1. Instalacja Restic (Ubuntu/Debian)

# Snap psuje dependencies, używaj github releases
wget https://github.com/restic/restic/releases/download/v0.17.3/restic_0.17.3_linux_amd64.bz2
bunzip2 restic_*.bz2
sudo install -m 755 restic_* /usr/local/bin/restic
restic version  # 0.17.3 compiled with go1.23

2. Konto Backblaze + bucket

  • Załóż konto na backblaze.com (15 GB free, dalej 0.005 USD/GB/mc)
  • New Bucket: kkaletka-backup, private, encryption: server-side, region: eu-central
  • Application Key z scope tylko do tego bucketu (nie używaj master key)

3. Inicjalizacja repo

# Plik z env vars (nie commituj do repo!)
cat > /etc/restic/env <<EOF
B2_ACCOUNT_ID=your-key-id
B2_ACCOUNT_KEY=your-app-key
RESTIC_REPOSITORY=b2:kkaletka-backup:/
RESTIC_PASSWORD=$(openssl rand -base64 32)
EOF
chmod 600 /etc/restic/env
 
# Init (uruchom RAZ na całe życie repo!)
source /etc/restic/env
restic init

Krytyczne: zapisz RESTIC_PASSWORD w password managerze. Bez niego, repo bezużyteczne. Stracisz hasło → stracisz wszystko.

4. Skrypt backup

#!/usr/bin/env bash
# /usr/local/bin/backup-restic.sh
set -euo pipefail
 
source /etc/restic/env
HOST=$(hostname)
LOG=/var/log/restic-${HOST}.log
 
# 1. Postgres dump
pg_dump -U postgres -d betting_hope | \
  restic backup --stdin --stdin-filename "${HOST}-postgres-$(date +%F).sql" \
  --tag postgres --tag $HOST 2>&1 | tee -a $LOG
 
# 2. File system
restic backup \
  /home/kkaletka \
  /etc \
  /var/lib/docker/volumes \
  --exclude-file=/etc/restic/excludes \
  --tag files --tag $HOST 2>&1 | tee -a $LOG
 
# 3. Apply retention policy
restic forget --prune \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 12 2>&1 | tee -a $LOG
 
# 4. Integrity check (subset, full check 1x/mc)
restic check --read-data-subset=5% 2>&1 | tee -a $LOG

5. Plik exclude

# /etc/restic/excludes
node_modules
.next
__pycache__
*.pyc
*.tmp
*.log
**/cache/**
.cache
.git/objects/pack
target/debug
target/release

Wycina ~40% objętości. node_modules zwłaszcza, restoreujesz npm install, nie 800 MB JS bibliotek.

6. Systemd timer (zamiast cron)

# /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic backup
After=network.target
 
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-restic.sh
EnvironmentFile=/etc/restic/env
Nice=10
IOSchedulingClass=idle
 
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup at 03:30
 
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=15min
 
[Install]
WantedBy=timers.target
sudo systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer  # weryfikacja

Nice=10 + IOSchedulingClass=idle, backup nie zarzyna systemu. RandomizedDelaySec, nie wszystkie maszyny startują dokładnie 3:30.

Monitoring do Telegrama

Backup który nie woła gdy padnie = brak backupu.

# Dorzuć do /usr/local/bin/backup-restic.sh
TELEGRAM_TOKEN="your-bot-token"
CHAT_ID="your-chat-id"
 
notify_failure() {
  local msg="$1"
  curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
    -d "chat_id=${CHAT_ID}" \
    --data-urlencode "text=⚠️ Backup ${HOST} failed: ${msg}"
}
 
trap 'notify_failure "Skrypt padł na linii $LINENO"' ERR

set -e + trap, każde niezerowe exit z wnętrza skryptu pójdzie w Telegrama z numerem linii. Po roku używania mam ~3 alerty/rok (głównie B2 maintenance windows).

Restore — najważniejszy test

Backup nieprzetestowany = brak backupu (część 2).

# Listuj snapshots
restic snapshots --tag postgres
 
# Restore konkretny plik z konkretnego snapshota
restic restore latest --target /tmp/restore --include /home/kkaletka/.env
 
# Restore postgresa
restic dump latest /openclaw-master-postgres-2026-05-11.sql | \
  psql -U postgres -d betting_hope_restored

Robię test restore raz na miesiąc, do osobnej VM, sprawdzam że databasy działają. Zajmuje 15 minut, daje 100% pewność.

Realne koszty

B2 storage:           80 GB × 0.005 USD/GB/mc = 0.40 USD/mc
B2 download (test):   5 GB × 0.01 USD = 0.05 USD/mc
B2 API calls:         ~0.02 USD/mc
                      ─────────────────────────
                      ~0.47 USD/mc = ~2 zł/mc

Plus opcjonalny second backup do Hetzner Storage Box (1 TB za 12 zł/mc), przyda się jeśli B2 padnie globalnie. U mnie to:

# Dodatkowy snapshot weekly do Hetzner
restic -r sftp:[email protected]:/restic snapshots

3-2-1 zasada: 3 kopie, 2 różne media, 1 off-site. Production data → main disk + B2 + Hetzner.

Co bym zrobił inaczej

  • Wcześniej zaczął test restore. Pierwszy restore robiłem po 4 miesiącach od setupu, okazało się że RESTIC_PASSWORD się gubił przy kolejnych runach. Test od początku by to wyłapał.
  • Osobny bucket per maszyna. Trzymam wszystko w jednym, ale to znaczy że każda maszyna potencjalnie może czytać backupy innej. Z osobnymi bucketami i app keys per-host, better isolation.
  • Healthchecks.io zamiast tylko Telegram. Dla cron-style jobów healthchecks.io daje "ping that didn't come", wykryje jak skrypt się w ogóle nie odpalił (a nie tylko padł).

TL;DR — checklist

  • Restic + Backblaze B2 (5 zł/mc dla 80 GB)
  • RESTIC_PASSWORD w password managerze, nie tylko na dysku
  • Exclude node_modules, __pycache__, target/, oszczędza 40%
  • Retention --keep-daily 7 --keep-weekly 4 --keep-monthly 12
  • Systemd timer (Nice=10, IOSchedulingClass=idle, RandomizedDelaySec=15min)
  • Telegram/Healthchecks.io notify on failure
  • Test restore raz na miesiąc, do osobnej VM
  • Bonus: drugi off-site backup (Hetzner Storage Box)

Setup zajmuje 2-3h, kosztuje grosze, i daje spać. To ostatnia rzecz która ma być budgetem.