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, regioneu-central-003. - Auto-monitoring:
f/infra/backup_health_monitor.pyw Windmillu, schedule*/360min, 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:
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:
- Klucz
restic-passwordz SOPS pasuje do faktycznego repo (rotation regression). - Backrest faktycznie potrafi odczytać B2 (klucze API B2 nie wygasły).
- 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):
--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:
- Stop app:
docker compose stop <app>. - Restore PG:
pg_restore -U <user> -d <db> --clean <dump>(lubpsql < dump.sqldla starszych planów). - Restore wolumeny aplikacyjne (uploads, attachments) — z
tar.gzw snapshocie. - 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:
- Bitwarden cloud (
panel-aio / restic-password) — pierwszy stop. secrets/secrets.sops.yamlw repo — drugi (wymaga klucza age).- 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: 401przy 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¶
- Sekrety —
backrest_restic_password, klucz age. - Inwentarz Usług — tabela harmonogramów + retencji.
- Monitoring i Backupy — wysokopoziomowy obraz dla operatorów.
- Restart i recovery — kiedy restore vs. restart.
windmill/f/infra/backup_health_monitor.py— kod skryptu monitorującego.