11 posts 44 tags 7 domains

Backup Raspberry Pi na Hetzner Storage Box z restic

Kompletna konfiguracja zaszyfrowanego backupu Raspberry Pi na Hetzner Storage Box — od kluczy SSH po systemd timer.

Problem

Raspberry Pi służy mi jako mały serwer domowy — LAMP, kilka skryptów, konfigi. Karta SD lubi się sypać, więc backup to nie opcja, tylko obowiązek. Cel: codzienny przyrostowy backup szyfrowany po stronie klienta, trzymany off-site na Hetzner Storage Box, z rotacją snapshotów i bez ręcznego klikania.

Stack: restic (deduplikacja + szyfrowanie), SFTP (wbudowany w restic, nie potrzeba żadnego agenta), systemd timer (lepszy niż cron — Persistent, randomizacja, łatwe logi).

Instalacja restic

bash
$sudo apt update
$sudo apt install restic
$sudo restic self-update

self-update warto odpalić — wersja w apt bywa starsza.

Klucz SSH dla Storage Boxa

Wszystko robimy jako root — backup systemowy musi czytać /etc, /var, pliki różnych userów.

bash
$sudo -i
$ssh-keygen -t ed25519 -f /root/.ssh/hetzner_storagebox -N ""

-N "" = bez passphrase. Klucz musi działać bez interakcji, bo systemd nie ma jak go odblokować.

Wgranie klucza na Storage Box

Jeśli w nowym UI nie widać opcji dodania klucza, najszybciej przez SFTP — np. WinSCP albo z terminala:

bash
$ssh -p23 u123456@u123456.your-storagebox.de mkdir -p .ssh
$cat /root/.ssh/hetzner_storagebox.pub | \
ssh -p23 u123456@u123456.your-storagebox.de install-ssh-key

install-ssh-key to wbudowany skrypt Hetznera — dopisuje klucz do authorized_keys z poprawnymi uprawnieniami.

Ręczne wgranie przez WinSCP też działa — pamiętaj o uprawnieniach po stronie Storage Boxa: .ssh → 700, authorized_keys → 600. Bez tego SSH zignoruje plik i nadal będzie pytać o hasło.

Konfiguracja SSH dla restica

/root/.ssh/config:

text
Host *.your-storagebox.de
    User u123456
    Port 23
    IdentityFile /root/.ssh/hetzner_storagebox
    ServerAliveInterval 60
bash
$chmod 600 /root/.ssh/config

Test — pierwsze połączenie zaakceptuje fingerprint i wpisze go do known_hosts:

bash
$ssh -p23 -i /root/.ssh/hetzner_storagebox u123456@u123456.your-storagebox.de

Powinno wpuścić bez pytania o hasło. Storage Box ma ograniczony shell — to OK, restic używa SFTP w tle.

Repo i hasło

Hasło repozytorium (zaszyfruje wszystko po stronie klienta):

bash
$openssl rand -base64 32 > /root/.restic-password
$chmod 600 /root/.restic-password

Zmienne środowiskowe — /root/.restic-env:

bash
export RESTIC_REPOSITORY="sftp:u123456@u123456.your-storagebox.de:/home/backups/raspi"
export RESTIC_PASSWORD_FILE="/root/.restic-password"
bash
$chmod 600 /root/.restic-env

Inicjalizacja repo — robi się tylko raz:

bash
$source /root/.restic-env
$restic init

Skrypt backupu

/usr/local/bin/restic-backup.sh:

bash
#!/bin/bash
set -euo pipefail

source /root/.restic-env

BACKUP_PATHS=(
    /etc
    /home
    /var/www
    /root
    /opt
)

EXCLUDES=(
    --exclude-caches
    --exclude='/home/*/.cache'
    --exclude='*.tmp'
    --exclude='/var/www/*/cache'
    --exclude='/var/www/*/var/cache'
    --exclude='node_modules'
)

restic backup \
    "${BACKUP_PATHS[@]}" \
    "${EXCLUDES[@]}" \
    --tag auto \
    --host raspi

restic forget \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 6 \
    --prune

if [ "$(date +%u)" -eq 7 ]; then
    restic check --read-data-subset=10%
fi
bash
$chmod +x /usr/local/bin/restic-backup.sh

--read-data-subset=10% w niedzielę realnie ściąga i weryfikuje 10% danych — wykryje uszkodzenie po stronie Storage Boxa, którego sam forget nie zauważy.

Systemd timer

/etc/systemd/system/restic-backup.service:

ini
[Unit]
Description=Restic backup to Hetzner Storage Box
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/restic-backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

/etc/systemd/system/restic-backup.timer:

ini
[Unit]
Description=Daily restic backup

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30min
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true = jeśli Pi było wyłączone o 3:30, backup ruszy zaraz po starcie. Cron tego nie potrafi.

bash
$systemctl daemon-reload
$systemctl enable --now restic-backup.timer
$systemctl list-timers restic-backup.timer

Test ręczny

Timer to tylko harmonogram — żeby ręcznie odpalić backup, uruchamiasz service:

bash
#Terminal 1 — log na żywo
$sudo journalctl -u restic-backup.service -f
#Terminal 2 — wystrzał
$sudo systemctl start restic-backup.service

Pierwszy backup pójdzie wolno (Storage Boxy nie są demonem prędkości), kolejne lecą tylko delty.

Quality of life

Żeby restic snapshots działał z marszu po sudo -i, dopisz do /root/.bashrc:

bash
[ -f /root/.restic-env ] && source /root/.restic-env

Co się dzieje pod spodem

Restic dzieli pliki na bloki ~1 MB, oblicza SHA-256 każdego, szyfruje AES-256 i wysyła tylko bloki, których jeszcze nie ma w repo. Snapshot to lekki manifest wskazujący na bloki — stąd kosztuje grosze nawet przy codziennej rotacji. SFTP po stronie restica to natywny klient w Go, bez wywoływania zewnętrznego sftp — używa tylko OpenSSH-owej konfiguracji do nawiązania połączenia.