Przejdź do treści

Backupy — weryfikacja i restore

System: Backrest (UI nad restic) + Backblaze B2 (off-site). Wszystkie krytyczne usługi mają plan backupu — dziennie, retencja 14–30 dni. Klucz szyfrujący: backrest_restic_password w secrets/secrets.sops.yaml.

Krótko

  • UI: backrest.aiofactory.pl — wymaga Netbird VPN.
  • Storage: Backblaze B2, bucket aiofactory, region eu-central-003.
  • Auto-monitoring: f/infra/backup_health_monitor.py w Windmillu, schedule */360 min, alarm gdy snapshot starszy niż 26 h.
  • Drill weryfikacyjny: raz w miesiącu (przeczytaj cały rozdział).

Harmonogram backupów

Pełna tabela: Inwentarz Usług → Backupy. Wyciąg:

Cel Częstotliwość Retencja Plan ID w Backrest
Convex (snapshot) 03:00 30 dni dziennych + 12 tygodniowych convex-daily
Mailcow (skrzynki + konfig) 02:00 14 dni mailcow-daily
Windmill (db + skrypty) 04:00 (db); per-push (skrypty/git) 14 dni B2 + git wieczne windmill-db-daily
WordPress ×4 (db + uploads) 01:00 30 dni wordpress-<site>-daily
Postiz (db) 04:00 14 dni postiz-daily
Listmonk (db + listy) 04:30 14 dni listmonk-daily
Forgejo (repo + db) 04:30 30 dni forgejo-daily
Beszel (db metryk) 05:00 7 dni beszel-daily

Weryfikacja — czy backupy się robią

Trzy niezależne kanały sprawdzania. Używaj wszystkich na zmianę.

1. Backrest UI — codzienna kontrola wzrokowa

backrest.aiofactory.pl (Netbird VPN required).

  • Plans — lista planów + status ostatniego runa (zielony/czerwony badge).
  • Snapshots — kliknij plan → zobacz lista snapshotów per repo.
  • Activity — feed recent operations (backup, prune, check).

Sygnał alarmowy: ostatni snapshot starszy niż 25 h dla planu daily, albo czerwony badge Failed przy ostatnim runie.

2. f/infra/backup_health_monitor.py — automatyczny watchdog

Skrypt Windmillowy odpalany co 6 h:

wmill script run f/infra/backup_health_monitor -d '{}' -s

Zwraca strukturę:

{
  "status": "ok",            // lub "alert"
  "checked_at": "2026-05-02T...",
  "plan_count": 8,
  "plans": [
    {"plan": "convex-daily", "status": "ok", "age_hours": 5.2, "snapshot_count": 30, ...},
    {"plan": "mailcow-daily", "status": "stale", "age_hours": 27.4, "errors": ["Last backup 27.4h ago (threshold: 26h)"], ...}
  ],
  "summary": {"convex-daily": "ok", "mailcow-daily": "stale", ...}
}

status: alert → eskalacja. Skrypt jest pod-pięty pod scheduler Windmillowy (backup_health_monitor.schedule.yaml). Aktualnie alarm idzie tylko do logów Windmilla — można rozbudować o powiadomienie SMS / email gdy jakiś vendor zacznie regularnie się sypać (zgłoszenie do backloga: TODO).

3. B2 console — raz w kwartale

Zaloguj się do Backblaze B2 (b2.aiofactory.pl/login → SSO Bitwarden). Otwórz bucket aiofactory → sprawdź:

  • Total bytes — czy rośnie liniowo (≈ +stały delta dziennie)?
  • File counts — czy nie zatrzymał się stary plan (kontroluj per b2://aiofactory/<plan-id>/...)?
  • Last upload — sprawdź pliki z ostatnich 24 h.

Jeśli widzisz, że B2 nie dostaje plików, Backrest UI jest zielony, a backup_health_monitor.py mówi ok — masz problem z konfiguracją Backresta (np. plan zapisuje lokalnie, ale repo B2 jest wyłączone). Otwórz Backrest UI → Repos → sprawdź b2: URL i credentials.


Drill weryfikacyjny — raz w miesiącu

Sucha weryfikacja ≠ działanie. Co miesiąc wykonaj prawdziwy restore mały, na sandbox, żeby upewnić się, że:

  1. Klucz restic-password z SOPS pasuje do faktycznego repo (rotation regression).
  2. Backrest faktycznie potrafi odczytać B2 (klucze API B2 nie wygasły).
  3. Stan plików jest spójny — restic check wykrywa korupcje.

Procedura drill (~30 min)

# Z Twojego laptopa, w Netbird VPN:

# 1. Decrypt restic-password
PASS=$(sops --decrypt --extract '["backrest_restic_password"]' secrets/secrets.sops.yaml)

# 2. Lokalny test restore — wybierz mały plan (np. listmonk-daily)
mkdir -p /tmp/drill-listmonk
RESTIC_PASSWORD="$PASS" restic -r b2:aiofactory:/listmonk-daily snapshots --limit 5
RESTIC_PASSWORD="$PASS" restic -r b2:aiofactory:/listmonk-daily restore latest --target /tmp/drill-listmonk

# 3. Sanity check — czy są tabele Postgresa?
ls -la /tmp/drill-listmonk/

# 4. (Opcjonalnie) restore testowo do sandbox-listmonk DB:
#    psql -U listmonk -d listmonk_drill < /tmp/drill-listmonk/dump.sql

# 5. Cleanup
rm -rf /tmp/drill-listmonk

Wynik: PASS, jeśli restic snapshots listuje migawki + restic restore rozpakowuje pliki bez błędów.

restic check na konkretnym repo (full integrity scan, ~10 min dla 5 GB):

RESTIC_PASSWORD="$PASS" restic -r b2:aiofactory:/convex-daily check --read-data-subset=10%

--read-data-subset=10% losuje 10 % packów do pełnej deserializacji — kompromis czas/pewność. Pełny check --read-data zajmie godziny dla wszystkich repo łącznie.


Per-service runbooks

Convex — restore z restic snapshot

Convex eksportuje snapshot codziennie 03:00 jako .tar.gz zawierający dump tablic + binarne pliki storage.

# 1. Otrzymaj snapshot
PASS=$(sops --decrypt --extract '["backrest_restic_password"]' secrets/secrets.sops.yaml)
mkdir -p /tmp/convex-restore
RESTIC_PASSWORD="$PASS" restic -r b2:aiofactory:/convex-daily restore latest --target /tmp/convex-restore

# 2. Skopiuj na host-dad
scp /tmp/convex-restore/snapshot-*.tar.gz aio-dad:/tmp/

# 3. Zaimportuj — convex import (CLI)
ssh aio-dad
sudo incus exec panel -- bash -c '
cd /root/dapps && \
docker compose exec convex-backend ./run-import.sh /var/snapshots/snapshot-XYZ.tar.gz
'
# UWAGA: dokładny shape import-skryptu zależy od wersji Convex backendu —
# patrz dokumentacja Convex self-hosted: https://docs.convex.dev/self-hosting

Convex restore zastępuje stan — utrata zmian po dacie snapshotu

Convex import jest pełen — kasuje wszystko, co weszło po dacie snapshotu. Pamiętaj o flagach: import najpierw na sandbox (drugi kontener convex-backend uruchomiony tymczasowo), porównanie diff, dopiero potem swap na produkcję.

Mailcow — restore skrzynek

Mailcow ma własny helper-scripts/backup_and_restore.sh:

ssh aio-mom
sudo incus exec email -- bash -c '
cd /root/dapps/mailcow-dockerized && \
helper-scripts/backup_and_restore.sh restore /var/backup/mailcow/<snapshot-dir>
'

Snapshoty Mailcowa są w /var/backup/mailcow/ w kontenerze email, replikowane do B2 przez Backrest plan mailcow-daily. Pełny restore: ~45 min.

Per-skrzynka (mniejszy zakres): pobierz vmail/<domain>/<user>/Maildir/ z snapshotu i wgraj do działającego kontenera bezpośrednio.

Windmill DB — restore Postgresa

Windmill kolejka jobów + metadane są w PostgreSQL 16 w kontenerze panel. Restore:

ssh aio-dad
sudo incus exec panel -- bash -c '
cd /root/dapps && \
docker compose stop windmill-server windmill-worker-* windmill-worker-native && \
docker compose exec windmill-db pg_restore -U windmill -d windmill --clean /var/backups/<latest>.dump && \
docker compose start windmill-server windmill-worker-*
'

Stop workerów PRZED restore'em DB

Workery próbują commitować jobs do DB — restore w trakcie aktywnych workerów = corruption. Stop wszystkie 8 workerów + native + server, potem pg_restore, potem start.

WordPress — restore (per sklep)

Każdy sklep WP ma własny plan: wordpress-master-daily, wordpress-eu-daily, etc. Restore:

PASS=$(sops --decrypt --extract '["backrest_restic_password"]' secrets/secrets.sops.yaml)

# 1. Restore z B2 do tmp
RESTIC_PASSWORD="$PASS" restic -r b2:aiofactory:/wordpress-master-daily restore latest --target /tmp/wp-master

# 2. Wgraj DB dump
ssh aio-mom
sudo incus exec web -- bash -c '
cd /root/dapps/buyspace-master && \
docker compose exec -T buyspace-master-db mysql -u root -p<root-pass> wordpress < /tmp/dump.sql
'

# 3. Wgraj uploads/
sudo incus exec web -- tar -xzf /tmp/wp-master/uploads.tar.gz -C /var/lib/docker/volumes/buyspace_master_uploads/_data/

Forgejo / Listmonk / Postiz — wzorzec ogólny

Wszystkie są stosami app + Postgres. Wzorzec:

  1. Stop app: docker compose stop <app>.
  2. Restore PG: pg_restore -U <user> -d <db> --clean <dump> (lub psql < dump.sql dla starszych planów).
  3. Restore wolumeny aplikacyjne (uploads, attachments) — z tar.gz w snapshocie.
  4. Start app: docker compose start <app>.

Utrata restic-password — recovery

Bez backrest_restic_password nie odzyskasz NICZEGO z B2

Każdy plik w B2 jest zaszyfrowany lokalnie przez restic kluczem chunk powiązanym z hasłem. B2 widzi tylko zaszyfrowane bloby. Klucz B2 (API key) tylko dostarcza pliki — nie deszyfruje.

Backupy klucza:

  1. Bitwarden cloud (panel-aio / restic-password) — pierwszy stop.
  2. secrets/secrets.sops.yaml w repo — drugi (wymaga klucza age).
  3. Backup kluczy age + restic: wydruk QR + offline na USB w sejfie. Zalecane dla solo-operatora.

Jeśli zgubisz: stary backup jest nieodzyskiwalny. Plan B (jeśli to się wydarzy): 1. Aktywuj host-son z najnowszą replikacją stanu (jeśli jest). 2. Re-build z gita: dashboard / windmill / convex schema są w repo. Stan baz danych jest nie do odtworzenia. 3. Zacznij nową kolejkę backupów z nowym hasłem.

Procedura rotacji hasła restic jest opisana w Sekrety → Rotacja — uwaga, nie jest to operacja in-place; nowy klucz dodaje się obok starego.


B2 — utrata bucketu / klucza API

Mniej krytyczne, ale operacyjne:

  • Klucz API B2 wygaśnie / zostanie odwołany: backupy przestaną się robić. Backrest UI pokaże Failed: 401 przy każdym planie. Fix: nowy klucz w B2 console → Backrest UI → Repos → update credentials → test → save.
  • Bucket usunięty (np. accident operator klikł): B2 ma soft-delete na 7 dni — szybko odzyskać przez B2 console. Po 7 dniach: nieodzyskiwalne.
  • B2 strona awaria: backupy danego dnia zostaną pominięte. Backrest planuje retry przy następnym tick — usuwa stale-by-1-day false positives. Jeśli >24 h B2-down, alert przez backup_health_monitor.py.

Powiązane