Kleine correcties bovenop ab87c0f, gevonden tijdens de eerste install
op scrum4me-srv (zie docs/runbooks/server-backup.md addendum):
- restic-backup.env.example: NAS-pad → /mnt/nas/backups/restic/scrum4me-srv,
Forgejo-container → scrum4me-forgejo (waren placeholders die niet matchten
met de actuele server-state).
- server-backup.service: ReadWritePaths uitgebreid met /mnt/nas/backups —
ProtectSystem=strict blokkeerde anders schrijven naar de NAS-repo.
RequiresMountsFor=/mnt/nas/backups toegevoegd om cifs-automount te triggeren
bij timer-fire. Documentation=-URL gecorrigeerd naar /srv/scrum4me/.
- server-backup.sh: --skip-db verwijderd uit forgejo dump (Forgejo 11.x heeft
die flag niet meer; DB komt nu mee in de zip, redundant met de aparte
forgejo_db_dump-fase maar onschuldig).
- server-backup.sh: subshell-bug in determine_exit_code gefixt — werd
aangeroepen via $(...), dus OVERALL_STATUS lekte niet naar de parent
en write_status_json schreef altijd "unknown".
- restore-test.sh: --include filter toegevoegd op de assertion-paden — een
full restore (~476 GiB logical) liep direct vol op /tmp (7.6 GB tmpfs)
met 3.3M ENOSPC-errors. Nu 59 MiB in 10s.
- runbook: paden /srv/ops/repos/... → /srv/scrum4me/ops-dashboard/...,
<forgejo>-placeholders → scrum4me-forgejo, concrete cifs-prefixpath
fstab-regel in Deel A3, en een gevuld addendum met alle bevindingen
van de eerste install (B2-bucket-naam ScrumForMeSrvBackup, sudo -E quirk,
storage-cap incident, dedup-cijfers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
562 lines
28 KiB
Markdown
562 lines
28 KiB
Markdown
# Server-brede backup (restic + NAS + B2, dashboard-bediend)
|
||
|
||
## Context
|
||
|
||
`scrum4me-srv` draait een Docker-stack (Scrum4Me-web, worker-idea, ops-dashboard,
|
||
postgres-17, caddy) plus Forgejo. De huidige backup-dekking — alleen
|
||
`pg_dump ops_dashboard` naar `/srv/ops/backups/` met 30 dagen retentie op één
|
||
disk — laat **alles anders** vallen: Scrum4Me-data, Forgejo, Caddy-certs,
|
||
Docker-volumes en `/etc` zijn weg bij brand, diefstal, ransomware of disk-fail.
|
||
|
||
Doel: de server **herbouwbaar** maken vanuit een encrypted, gededupliceerde,
|
||
versioned backup met twee onafhankelijke kopieën — **NAS** lokaal en
|
||
**Backblaze B2** offsite — bediend vanuit de ops-dashboard. De bestaande
|
||
`backup_ops_db`-flow blijft draaien; restic pickt zijn dump-directory mee.
|
||
|
||
**Belangrijke ontwerpkeuzes** (uitgebreid toegelicht in de review onder
|
||
`/Users/janpetervisser/Development/Scrum4Me/docs/recommendations/server-backup-plan-review-2026-05-15.md`):
|
||
|
||
- **B2 Object Lock + server-key zonder `deleteFiles`** — een aanvaller met root
|
||
op de server kan geen B2-snapshots weghalen tot Object Lock-retention
|
||
verloopt. Dat is de ransomware-bescherming. Prune op B2 gebeurt maandelijks
|
||
vanaf de laptop met een aparte hoge-cap maintenance-key.
|
||
- **Authoritative restore-bron = dumps, niet live datadirs.** Postgres- en
|
||
Forgejo-data-directories zijn expliciet `--exclude`'d uit restic;
|
||
`pg_dumpall` en `forgejo dump` + aparte `pg_dump <forgejo_db>` zijn de
|
||
autoritatieve bronnen.
|
||
- **Phase-based script met structured statusfile.** Eén falende fase laat de
|
||
rest doorlopen; per-phase status / exit-code / timestamps / error-tail komen
|
||
in `/srv/backups/status/last-run.json` die de dashboard live leest.
|
||
- **Single-instance lock** via `flock /run/server-backup.lock` — UI-knop en
|
||
systemd-timer kunnen elkaar niet overlappen.
|
||
|
||
## Voorwaarden (aantoonbaar voldaan vóór uitvoering)
|
||
|
||
- [ ] Bash, jq, restic, docker, gzip, flock op `$PATH` (`apt install restic jq` voor de eerste twee — de rest zit standaard).
|
||
- [ ] De Scrum4Me-stack draait in Docker (`docker ps | grep scrum4me-postgres`).
|
||
- [ ] `/srv/scrum4me/compose/docker-compose.yml` bestaat (anders herzie je het exclude-pad in `server-backup.sh`).
|
||
- [ ] Tijd loopt synchroon (`timedatectl status`) — backups gebruiken ISO-timestamps.
|
||
|
||
## Voorwaarden (input van de gebruiker nodig)
|
||
|
||
- **NAS-mount** — pad zoals `/mnt/nas/backups` met genoeg ruimte (initieel ≥ 100 GB; restic is gededupliceerd, dus daarna groeit het traag).
|
||
- **Backblaze B2-account** — credit-card geregistreerd, bucket aanmaken vereist een operator-actie.
|
||
- **Restic-wachtwoord** — `openssl rand -hex 24`, bewaard in je password manager **én** in `/etc/restic-backup.password` op de server. Beide nodig — kwijt op één plek = repo onleesbaar.
|
||
- **B2 maintenance-key** — bewaard alleen op je laptop in passwordmanager. Niet op de server.
|
||
|
||
---
|
||
|
||
## Deel A — Voorbereiding op `scrum4me-srv`
|
||
|
||
Uit te voeren als `root` op `scrum4me-srv`.
|
||
|
||
1. **Tools installeren**
|
||
```bash
|
||
sudo apt update
|
||
sudo apt install -y restic jq
|
||
restic version
|
||
```
|
||
|
||
2. **Directories aanmaken**
|
||
```bash
|
||
sudo mkdir -p /srv/backups/scripts /srv/backups/logs /srv/backups/status \
|
||
/var/backups/databases
|
||
sudo chmod 0750 /srv/backups/logs /srv/backups/status
|
||
```
|
||
|
||
3. **NAS-mount aanmaken** — een nieuwe mount op `/mnt/nas/backups` die naar
|
||
de subdir `backups` van de bestaande `ssd`-share op de NAS wijst. Geen
|
||
nieuwe Samba-share op de NAS nodig — de cifs-`prefixpath`-optie mount het
|
||
subpad direct.
|
||
|
||
```bash
|
||
# 1. Subdir op de NAS aanmaken via de bestaande ssd-mount
|
||
sudo mkdir -p /mnt/nas/ssd/backups
|
||
|
||
# 2. cifs-utils geïnstalleerd? (voor andere /mnt/nas-shares is dat al zo)
|
||
dpkg -l | grep -q '^ii cifs-utils' || sudo apt install -y cifs-utils
|
||
|
||
# 3. Fstab-regel toevoegen — uid/gid=0 + mode 0700/0600 = root-only
|
||
sudo tee -a /etc/fstab <<'EOF'
|
||
//192.168.0.155/ssd /mnt/nas/backups cifs credentials=/etc/samba/credentials-nas,uid=0,gid=0,iocharset=utf8,vers=3.0,nofail,_netdev,x-systemd.automount,prefixpath=backups,file_mode=0600,dir_mode=0700 0 0
|
||
EOF
|
||
|
||
# 4. systemd reload + mount
|
||
sudo systemctl daemon-reload
|
||
sudo mount /mnt/nas/backups
|
||
mountpoint -q /mnt/nas/backups && echo "OK" || echo "FAIL"
|
||
df -h /mnt/nas/backups
|
||
```
|
||
|
||
`_netdev,x-systemd.automount,nofail` zorgt dat de mount automatisch terugkomt
|
||
bij reboot zónder de boot te laten hangen als de NAS even weg is. De
|
||
`RequiresMountsFor=/mnt/nas/backups` in `server-backup.service` triggert
|
||
bovendien de automount voor de timer-run.
|
||
|
||
4. **Restic-wachtwoord genereren en plaatsen**
|
||
```bash
|
||
sudo sh -c 'openssl rand -hex 24 > /etc/restic-backup.password'
|
||
sudo chmod 0400 /etc/restic-backup.password
|
||
sudo chown root:root /etc/restic-backup.password
|
||
```
|
||
**Kopieer dezelfde string naar je password manager** vóór je verder gaat. Een gegeneerd wachtwoord dat alleen op de server staat is geen wachtwoord — het is een ticking time bomb.
|
||
|
||
---
|
||
|
||
## Deel B — Backblaze B2 inrichten (Object Lock + scoped keys)
|
||
|
||
Doel: een bucket waarvan **bestaande** snapshots niet door de server gewist kunnen worden, plus twee separate keys: één voor de server (alleen schrijven/lezen) en één voor de operator (alle rechten, alleen vanaf laptop gebruikt).
|
||
|
||
1. **Bucket aanmaken** in de Backblaze-UI of via `b2` CLI:
|
||
- Naam: `scrum4me-srv-backup` (of een variant; vermeld in `/etc/restic-backup.env`).
|
||
- Privacy: **Private**.
|
||
- **File Lock: Enabled, Governance mode, default retention = 30 days**. Governance betekent: een key met `bypassGovernance` kan locks omzeilen — die capability geven we **alleen** aan de maintenance-key.
|
||
- Lifecycle rules: **geen** (lifecycle conflicts met Object Lock).
|
||
- Encryption: server-side encryption aanlaten (B2 standaard).
|
||
|
||
2. **Server-key** aanmaken (gaat naar `/etc/restic-backup.env` op de server):
|
||
```bash
|
||
# via b2 CLI:
|
||
b2 application-key create \
|
||
--bucket scrum4me-srv-backup \
|
||
--name-prefix scrum4me-srv \
|
||
server-backup-key \
|
||
listBuckets,listFiles,readFiles,writeFiles
|
||
```
|
||
Bewaar de output (`keyID` + `applicationKey`). Verifieer in de UI dat de key **niet** `deleteFiles`, **niet** `deleteKeys`, **niet** `bypassGovernance` heeft.
|
||
|
||
3. **Maintenance-key** aanmaken (gaat in je password manager op de laptop):
|
||
```bash
|
||
b2 application-key create \
|
||
--bucket scrum4me-srv-backup \
|
||
scrum4me-srv-maintenance-key \
|
||
listBuckets,listFiles,readFiles,writeFiles,deleteFiles,bypassGovernance
|
||
```
|
||
Deze key komt **nooit** op de server. Gebruik alleen voor `restic forget --prune` vanaf je laptop (zie Deel H).
|
||
|
||
4. **`/etc/restic-backup.env` aanmaken**
|
||
```bash
|
||
sudo cp /srv/scrum4me/ops-dashboard/deploy/server-backup/restic-backup.env.example \
|
||
/etc/restic-backup.env
|
||
sudo chmod 0600 /etc/restic-backup.env
|
||
sudo chown root:root /etc/restic-backup.env
|
||
sudo nano /etc/restic-backup.env
|
||
```
|
||
Vul in: `RESTIC_REPO_NAS`, `RESTIC_REPO_B2`, `B2_ACCOUNT_ID` (= keyID), `B2_ACCOUNT_KEY` (= applicationKey). Forgejo-velden in Deel F.
|
||
|
||
**Dreigingsmodel**
|
||
|
||
| Dreiging | Gedekt door dit ontwerp? |
|
||
|---|---|
|
||
| Disk-fail / corruptie | ✓ NAS + B2 = 2× redundancy |
|
||
| Brand / diefstal / waterschade | ✓ B2 is offsite |
|
||
| Ransomware op de server | ✓ B2 Object Lock — bestaande snapshots immutable tot retention verloopt |
|
||
| Server-compromise (root) | ✓ server-key kan geen B2-files verwijderen |
|
||
| Laptop-compromise + server-compromise simultaan | ✗ maintenance-key dan ook in handen van aanvaller — geen verdediging |
|
||
| Backblaze account-compromise | ✗ — buiten scope; mitigeer met 2FA en audit-trail |
|
||
| Verlies restic-wachtwoord | ✗ — repos onleesbaar; bewaar wachtwoord óók in password manager |
|
||
|
||
---
|
||
|
||
## Deel C — Restic-repos initialiseren
|
||
|
||
1. **NAS-repo init**
|
||
```bash
|
||
sudo -E bash -c '
|
||
set -a; . /etc/restic-backup.env; set +a
|
||
export RESTIC_PASSWORD_FILE=/etc/restic-backup.password
|
||
restic -r "$RESTIC_REPO_NAS" init
|
||
'
|
||
```
|
||
|
||
2. **B2-repo init**
|
||
```bash
|
||
sudo -E bash -c '
|
||
set -a; . /etc/restic-backup.env; set +a
|
||
export RESTIC_PASSWORD_FILE=/etc/restic-backup.password
|
||
restic -r "$RESTIC_REPO_B2" init
|
||
'
|
||
```
|
||
|
||
3. **Retentie droogtest** — controleer dat het forget-beleid niet té agressief is op een eerste-snapshot-only repo. (Op een verse repo verwijdert `forget` niets, maar dit toont dat alle paden + auth werken.)
|
||
```bash
|
||
sudo -E bash -c '
|
||
set -a; . /etc/restic-backup.env; set +a
|
||
export RESTIC_PASSWORD_FILE=/etc/restic-backup.password
|
||
restic -r "$RESTIC_REPO_NAS" forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --dry-run
|
||
'
|
||
```
|
||
|
||
---
|
||
|
||
## Deel D — Scripts en systemd-units plaatsen
|
||
|
||
1. **Scripts kopiëren**
|
||
```bash
|
||
sudo cp /srv/scrum4me/ops-dashboard/deploy/server-backup/server-backup.sh /srv/backups/scripts/
|
||
sudo cp /srv/scrum4me/ops-dashboard/deploy/server-backup/restore-test.sh /srv/backups/scripts/
|
||
sudo chmod 0750 /srv/backups/scripts/*.sh
|
||
sudo chown root:root /srv/backups/scripts/*.sh
|
||
```
|
||
|
||
2. **Systemd-units kopiëren**
|
||
```bash
|
||
sudo cp /srv/scrum4me/ops-dashboard/deploy/server-backup/server-backup.service /etc/systemd/system/
|
||
sudo cp /srv/scrum4me/ops-dashboard/deploy/server-backup/server-backup.timer /etc/systemd/system/
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now server-backup.timer
|
||
```
|
||
|
||
3. **Timer verifiëren**
|
||
```bash
|
||
systemctl list-timers | grep server-backup
|
||
```
|
||
Toont next-run morgen 03:30 (+ randomized delay tot 10 min).
|
||
|
||
---
|
||
|
||
## Deel E — Eerste run handmatig + statusfile-verificatie
|
||
|
||
1. **Trigger**
|
||
```bash
|
||
sudo systemctl start server-backup.service
|
||
```
|
||
|
||
2. **Live volgen**
|
||
```bash
|
||
journalctl -u server-backup.service -f
|
||
```
|
||
Verwacht: 8 fasen (postgres_dump, forgejo_dump, forgejo_db_dump, restic_nas, restic_b2, forget_nas, check_nas, check_b2), elk met een `─── phase: X ───` start- en `─── end X (exit=N, status=S)` eindregel.
|
||
|
||
3. **Statusfile**
|
||
```bash
|
||
sudo jq . /srv/backups/status/last-run.json
|
||
```
|
||
Verwacht: `overall_status: "success"`, alle 5 verplichte fasen `success` (Forgejo mag `skipped` zijn als die nog niet geconfigureerd is).
|
||
|
||
4. **Snapshots**
|
||
```bash
|
||
sudo -E bash -c '
|
||
set -a; . /etc/restic-backup.env; set +a
|
||
export RESTIC_PASSWORD_FILE=/etc/restic-backup.password
|
||
restic -r "$RESTIC_REPO_NAS" snapshots
|
||
restic -r "$RESTIC_REPO_B2" snapshots
|
||
'
|
||
```
|
||
Beide tonen één snapshot met `host=scrum4me-srv` en tags `scheduled`.
|
||
|
||
---
|
||
|
||
## Deel F — Forgejo subplan
|
||
|
||
Vóór de eerste full-backup run: inventariseer Forgejo en bevestig (of corrigeer) de defaults in `restic-backup.env`. Bij twijfel — zet `FORGEJO_CONTAINER=` (leeg) zodat de Forgejo-fases als `skipped` markeren tot je verifieerd hebt.
|
||
|
||
### F1. Inventarisatie
|
||
|
||
```bash
|
||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep -i forgejo
|
||
```
|
||
|
||
Noteer:
|
||
- container-naam (vermoedelijk `forgejo`).
|
||
- image-versie (`codeberg.org/forgejo/forgejo:<versie>`).
|
||
|
||
### F2. Configpaden in de container
|
||
|
||
```bash
|
||
docker inspect scrum4me-forgejo --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ println }}{{ end }}'
|
||
docker exec scrum4me-forgejo ls -la /data/gitea/conf/app.ini
|
||
```
|
||
|
||
Standaard: `app.ini` in `/data/gitea/conf/app.ini` binnen de container. Wijkt dat af, pas `FORGEJO_CONFIG=` in `/etc/restic-backup.env` aan.
|
||
|
||
### F3. DB-koppeling controleren
|
||
|
||
```bash
|
||
docker exec scrum4me-forgejo grep -E '^DB_TYPE|^HOST|^NAME|^USER' /data/gitea/conf/app.ini
|
||
```
|
||
|
||
- `DB_TYPE=postgres` met `NAME=forgejo` ⇒ zet `FORGEJO_DB_NAME=forgejo`, en als de Postgres-container niet `scrum4me-postgres` is: `FORGEJO_DB_CONTAINER=...`.
|
||
- `DB_TYPE=sqlite` ⇒ laat `FORGEJO_DB_NAME=` leeg; SQLite-DB komt mee in `forgejo dump`.
|
||
|
||
### F4. Dump-strategie
|
||
|
||
Het script doet **drie** dingen voor Forgejo:
|
||
|
||
1. `forgejo dump --skip-db -c <config> --type zip -f -` — codebases, attachments, hooks, LFS metadata, etc.
|
||
2. Separate `pg_dump <forgejo_db>` — autoritatieve DB-restore-bron (Forgejo docs documenteren bekende import-issues bij DB-inhoud uit `forgejo dump`, daarom `--skip-db`).
|
||
3. Live datadirs (`/srv/forgejo/data/git`, `/srv/forgejo/data/lfs`, `/srv/forgejo/data/queues`) worden **niet** door restic gekopieerd — dat zijn live B-Trees waar een file-level kopie inconsistent zou zijn.
|
||
|
||
### F5. Restore-test in geïsoleerde compose-stack
|
||
|
||
Vóór je de Forgejo-restore voor real nodig hebt: test hem een keer. Maak een tijdelijke directory met een verse Forgejo + Postgres, voer de dumps in, draai `forgejo doctor check --all`.
|
||
|
||
```bash
|
||
# Minimaal restore-test-recept (vul in op basis van je Forgejo-versie)
|
||
RESTORE_DIR=/tmp/forgejo-restore-test
|
||
mkdir -p "$RESTORE_DIR"
|
||
cd "$RESTORE_DIR"
|
||
|
||
# 1. compose-stack met blanco Forgejo + Postgres
|
||
cat > docker-compose.yml <<'YAML'
|
||
services:
|
||
forgejo:
|
||
image: codeberg.org/forgejo/forgejo:<vul-versie-in>
|
||
volumes: [ "./forgejo-data:/data" ]
|
||
depends_on: [ db ]
|
||
db:
|
||
image: postgres:17
|
||
environment:
|
||
POSTGRES_USER: forgejo
|
||
POSTGRES_PASSWORD: testtest
|
||
POSTGRES_DB: forgejo
|
||
volumes: [ "./db-data:/var/lib/postgresql/data" ]
|
||
YAML
|
||
|
||
docker compose up -d
|
||
|
||
# 2. DB-dump terugzetten
|
||
gunzip < /var/backups/databases/forgejo-db-$(date +%F).sql.gz \
|
||
| docker compose exec -T db psql -U forgejo forgejo
|
||
|
||
# 3. Forgejo-dump uitpakken in de data-volume
|
||
docker compose stop forgejo
|
||
unzip /var/backups/databases/forgejo-$(date +%F).zip -d forgejo-data/
|
||
docker compose start forgejo
|
||
|
||
# 4. Health-checks
|
||
docker compose exec forgejo forgejo doctor check --all
|
||
curl -fsS http://localhost:3000/api/v1/version
|
||
```
|
||
|
||
Slaagt `forgejo doctor check --all` en het `/api/v1/version`-endpoint? Dan is je Forgejo-restore werkend. Tear-down: `docker compose down -v && rm -rf "$RESTORE_DIR"`.
|
||
|
||
---
|
||
|
||
## Deel G — Restore-procedure in productie
|
||
|
||
### G1. Files uit een snapshot terughalen
|
||
|
||
```bash
|
||
# Snapshot kiezen
|
||
sudo -E bash -c '
|
||
set -a; . /etc/restic-backup.env; set +a
|
||
export RESTIC_PASSWORD_FILE=/etc/restic-backup.password
|
||
restic -r "$RESTIC_REPO_NAS" snapshots
|
||
'
|
||
|
||
# Restore (latest, alleen /etc — voorbeeld)
|
||
sudo -E bash -c '
|
||
set -a; . /etc/restic-backup.env; set +a
|
||
export RESTIC_PASSWORD_FILE=/etc/restic-backup.password
|
||
restic -r "$RESTIC_REPO_NAS" restore latest --target /tmp/restore --include /etc
|
||
'
|
||
```
|
||
|
||
### G2. Postgres herstellen (Scrum4Me-cluster)
|
||
|
||
```bash
|
||
# Stop de apps die met de DB praten
|
||
docker compose -f /srv/scrum4me/compose/docker-compose.yml stop scrum4me-web ops-dashboard worker-idea
|
||
|
||
# Restore dumpall (drop + recreate alle DBs in de cluster — vandaar --clean --if-exists in de dump)
|
||
gunzip < /var/backups/databases/postgres-2026-05-15.sql.gz \
|
||
| docker exec -i scrum4me-postgres psql -U scrum4me
|
||
|
||
# Apps weer aan
|
||
docker compose -f /srv/scrum4me/compose/docker-compose.yml start scrum4me-web ops-dashboard worker-idea
|
||
```
|
||
|
||
Voor partial restore (alleen één database): pak die DB uit de dumpall-tekst met `pg_restore` of `awk`-block extractie. Voor alleen `ops_dashboard` is de bestaande [recovery.md](recovery.md) sectie 2a primair.
|
||
|
||
### G3. Forgejo herstellen
|
||
|
||
Volg [F5](#f5-restore-test-in-geïsoleerde-compose-stack) maar dan met de echte Forgejo-compose-stack en zonder tear-down. Belangrijk: stop de live Forgejo eerst, vervang `/srv/forgejo/data` volledig, restore DB, start Forgejo, `forgejo doctor check --all`.
|
||
|
||
---
|
||
|
||
## Deel H — Maintenance vanaf de laptop (maandelijks)
|
||
|
||
Doel: B2-snapshots ouder dan retention-policy daadwerkelijk pruning, plus een diepere integriteits-check die op de server te duur zou zijn.
|
||
|
||
1. **Voorbereiding** (eenmalig op laptop):
|
||
```bash
|
||
brew install restic jq
|
||
# Maintenance-key uit password manager
|
||
export B2_ACCOUNT_ID=<maintenance-key-id>
|
||
export B2_ACCOUNT_KEY=<maintenance-app-key>
|
||
export RESTIC_REPOSITORY=b2:scrum4me-srv-backup:scrum4me-srv
|
||
read -rs RESTIC_PASSWORD < /dev/tty # uit password manager
|
||
export RESTIC_PASSWORD
|
||
```
|
||
|
||
2. **Prune-check** (eerst dry-run om te zien wat er zou gebeuren):
|
||
```bash
|
||
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --dry-run
|
||
```
|
||
|
||
3. **Daadwerkelijke prune** (vereist `bypassGovernance` capability — alleen via maintenance-key):
|
||
```bash
|
||
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
|
||
```
|
||
|
||
4. **Diepere check**:
|
||
```bash
|
||
restic check --read-data-subset=10%
|
||
```
|
||
B2-bandbreedte: 10% van een 50 GB repo = 5 GB download, B2-prijs ~ $0.05 (gratis 1 GB/dag).
|
||
|
||
5. **Cleanup environment** — sluit shell of `unset RESTIC_PASSWORD B2_ACCOUNT_*`.
|
||
|
||
---
|
||
|
||
## Deel I — Integriteits-schedule (samenvatting)
|
||
|
||
| Cadans | Wie | Wat | Waarom |
|
||
|---|---|---|---|
|
||
| Dagelijks 03:30 | server (systemd timer) | `restic check` op beide repos | snelle metadata-/structure-validatie |
|
||
| Wekelijks (zondag) | server (zelfde script) | `restic check --read-data-subset=2.5%` op NAS, `1%` op B2 | sample-based data-integrity |
|
||
| Maandelijks | operator (laptop) | `restic check --read-data-subset=10%` + `forget --prune` op B2 | diepere check + prune (B2 server-key heeft geen delete-rechten) |
|
||
| Maandelijks | operator (server) | `/srv/backups/scripts/restore-test.sh nas` + handmatige Forgejo-stack-restore (F5) | end-to-end restore-verificatie |
|
||
|
||
---
|
||
|
||
## Te wijzigen / nieuw aangemaakte bestanden
|
||
|
||
**Op `scrum4me-srv`** (alleen via deploy uit deze repo, geen handmatige edits):
|
||
|
||
- `/srv/backups/scripts/server-backup.sh` (uit `deploy/server-backup/`).
|
||
- `/srv/backups/scripts/restore-test.sh` (idem).
|
||
- `/etc/systemd/system/server-backup.service`, `server-backup.timer` (uit `deploy/server-backup/`).
|
||
- `/etc/restic-backup.env` — secrets, niet in repo.
|
||
- `/etc/restic-backup.password` — secret, niet in repo.
|
||
|
||
**In deze repo (`ops-dashboard`)**, nieuw aangemaakt:
|
||
|
||
- `deploy/server-backup/*` — alle deploy-artefacten.
|
||
- `docs/runbooks/server-backup.md` — dit document.
|
||
- Later (Fase 3+4): `ops-agent/commands.yml.example`-uitbreiding, `ops-agent/flows.example/server_backup_*.yml`, `app/settings/backups/_components/server-backup-section.tsx`.
|
||
|
||
**Op de laptop**, in password manager:
|
||
|
||
- restic-wachtwoord (identiek aan `/etc/restic-backup.password`).
|
||
- B2 maintenance-key (keyID + applicationKey).
|
||
|
||
---
|
||
|
||
## Veelvoorkomende fouten
|
||
|
||
| Symptoom | Oorzaak | Fix |
|
||
|---|---|---|
|
||
| `unable to open repository ... no such file or directory` (NAS) | NAS-mount weg na reboot | `mountpoint -q /mnt/nas/backups` — fix `fstab`/`autofs`; herstart `server-backup.service` |
|
||
| `unable to open repository ... AccessDenied` (B2) | server-key heeft verkeerde capabilities of bucket-prefix | check `b2 application-key list`; capabilities moeten `listBuckets,listFiles,readFiles,writeFiles` zijn, name-prefix moet matchen |
|
||
| `Object Lock In Place` bij `forget --prune` op B2 | server probeert ten onrechte B2 te prunen (heeft die capability niet) | het script prune'd alleen NAS — als deze fout opduikt: handmatige `restic forget` op B2 gedraaid (zou off-server moeten); gebruik maintenance-key |
|
||
| `restic snapshot tag scheduled` ontbreekt in UI | run heeft `--tag scheduled` niet meegekregen | check script — `restic_backup_to` zet beide tags hardcoded |
|
||
| `forgejo dump` faalt met permission denied | container-user niet `git` | pas `dump_forgejo` aan: `docker exec -u <correct-user>` |
|
||
| restic exit code 3 in statusfile | sommige files waren niet leesbaar tijdens snapshot (open file lock) | non-fataal — log toont welke files; meestal logs of sockets; eventueel toevoegen aan `RESTIC_EXCLUDES` |
|
||
| `another server-backup is already running` exit 75 | timer en UI-knop tegelijk, of vorige run hangt | `systemctl status server-backup.service`; bij hang: `systemctl kill server-backup.service`, lockfile `/run/server-backup.lock` opruimen |
|
||
| `last-run.json` niet geüpdatet | script gecrashed vóór `write_status_json` | `journalctl -u server-backup.service --since=today` — meestal env-file of password-file probleem |
|
||
| Postgres-datadir in restic snapshot terug te zien | excludes verkeerd geconfigureerd | check `RESTIC_EXCLUDES` in script — moet `/srv/scrum4me/postgres` bevatten |
|
||
|
||
---
|
||
|
||
## Verificatie (end-to-end)
|
||
|
||
1. **Eerste run slaagt** — Deel E groen, statusfile `overall_status: success`.
|
||
2. **Snapshots zichtbaar** op beide repos via `restic snapshots`.
|
||
3. **Restore-test slaagt** — `restore-test.sh nas` → `overall_status: success` in `/srv/backups/status/last-restore-test.json`, alle assertions `ok`.
|
||
4. **Forgejo-restore-stack** (F5) — `forgejo doctor check --all` rond zonder errors, `/api/v1/version` antwoordt.
|
||
5. **Reboot-test** — server reboot, `systemctl list-timers` toont `server-backup.timer` met next-run gepland; NAS-mount automatisch terug.
|
||
6. **Failure-injectie**:
|
||
- NAS unmount → script eindigt met `overall_status: partial_failure`, `phases.restic_nas.status: failed`, B2-snapshot wel aanwezig, systemd exit 75.
|
||
- B2-key tijdelijk ongeldig → `phases.restic_b2.status: failed`, NAS-snapshot wel, exit 75.
|
||
- Beide repos onbereikbaar → `overall_status: failed`, exit 1.
|
||
7. **Concurrency** — tweede `systemctl start server-backup.service` tijdens lopende run → exit 75, log toont `another server-backup is already running`.
|
||
8. **Maandelijkse maintenance** — eerst keer succesvol uitgevoerd vanaf laptop, B2 `forget --prune` slaagt zonder Object Lock-fouten.
|
||
|
||
---
|
||
|
||
# Addendum — uitvoering 2026-05-15
|
||
|
||
Eerste install op `scrum4me-srv`. Werk begon met dat alleen `restic` + `jq`
|
||
geïnstalleerd waren — Deel A2-Deel E + restore-test draaiden in deze sessie.
|
||
|
||
## Vastgestelde topologie en concrete waarden
|
||
|
||
| Plan-placeholder | Werkelijkheid op `scrum4me-srv` |
|
||
|---|---|
|
||
| Repo-pad in deploy-stappen | `/srv/scrum4me/ops-dashboard/` (runbook had `/srv/ops/repos/...` — bestaat niet op deze server). Ook in `recovery.md` en `deploy/ops-dashboard-updater/update.sh` staan nog `/srv/ops/repos`-verwijzingen — losse cleanup-taak. |
|
||
| NAS-mount | `/mnt/nas/backups` via cifs-`prefixpath=backups` op `//192.168.0.155/ssd`. **Geen aparte Samba-share aangemaakt** — de subdir `backups/` op de bestaande `ssd`-share is genoeg dankzij `prefixpath`. fstab-regel: `uid=0,gid=0,prefixpath=backups,file_mode=0600,dir_mode=0700,_netdev,x-systemd.automount,nofail`. |
|
||
| B2-bucket-naam | **`ScrumForMeSrvBackup`** (PascalCase) — niet de in het plan voorgestelde `scrum4me-srv-backup`. `RESTIC_REPO_B2=b2:ScrumForMeSrvBackup:scrum4me-srv` (case-sensitive). |
|
||
| B2-bucket-instellingen | Object Lock = Enabled, Mode = Governance, Default Retention = 30 days. Geen lifecycle rules. |
|
||
| B2 server-key capabilities | `listBuckets,listFiles,readFiles,writeFiles` (gemaakt via webportal als "Read and Write" — daar zat de juiste capability-set automatisch in). Geen `deleteFiles`, geen `bypassGovernance`. |
|
||
| B2 storage-cap | $10/maand. Bij 16 GB op B2 (zie cijfers onder) is dat $0,10/maand storage — ruim binnen de cap. |
|
||
| B2 maintenance-key | **Nog niet aangemaakt** — pas nodig bij eerste maandelijkse prune. Aanmaken vanaf laptop, met `Allow file deletes` en `Allow bypass governance retention` aangevinkt. |
|
||
| Forgejo-container | **`scrum4me-forgejo`** (image `codeberg.org/forgejo/forgejo:11`). |
|
||
| Forgejo `git`-user | uid 1000, bestaat ✓ — `docker exec -u git scrum4me-forgejo` werkt. |
|
||
| Forgejo data-locatie | docker named volume `forgejo_forgejo-data` (NIET `/srv/forgejo/data/...` zoals het runbook nog noemt — die paden bestaan niet maar de excludes zijn no-ops). |
|
||
| Forgejo-DB | rol `forgejo`, db `forgejo`, in `scrum4me-postgres`-container (zelfde Postgres als ops_dashboard, scrum4me). |
|
||
| Postgres data live | bind-mount `/srv/scrum4me/postgres/` (excluded). |
|
||
| restic-password locatie | `/etc/restic-backup.password` (mode 0400, root:root). Óók in passwordmanager onder "restic — scrum4me-srv". |
|
||
| systemd-timer | `server-backup.timer` enabled, dagelijks 03:30 + max 10 min randomized delay. |
|
||
|
||
## Wijzigingen aan de in commit `ab87c0f` gemergede code
|
||
|
||
Door deze sessie heen vier kleine fixes nodig gebleken — allemaal in deze PR:
|
||
|
||
- **`server-backup.sh`**: `--skip-db` weggehaald uit `dump_forgejo`. Forgejo
|
||
11.x heeft die flag niet (verwijderd na de Gitea-fork). Output van
|
||
`forgejo dump --help`: alleen `--skip-repository|-log|-custom-dir|-lfs-data|-attachment-data|-package-data|-index|-repo-archives`. De DB komt nu mee in de zip — redundant met `forgejo_db_dump`-fase, maar onschuldig.
|
||
- **`server-backup.sh`**: subshell-bug in `determine_exit_code` — werd aangeroepen via `EXIT_CODE=$(determine_exit_code)`, dus `OVERALL_STATUS=...` werd in de subshell gezet en lekte niet naar de parent. Resultaat: `last-run.json` schreef altijd `overall_status: "unknown"` en de eind-banner idem. Fix: directe call die zowel `OVERALL_STATUS` als `EXIT_CODE` in de parent-shell zet.
|
||
- **`restore-test.sh`**: deed een **full restore** zonder `--include`-filter — probeerde 476 GiB naar `/tmp` (7.6 GB tmpfs) te schrijven, ENOSPC + 3.3M errors + alle assertions "missing". Gefixt met `--include` op alleen de assertion-paden (`/etc/restic-backup.env`, `/srv/scrum4me/{compose/docker-compose.yml,caddy/Caddyfile}`, `/var/backups/databases`). Restore is nu 59 MB in 10s.
|
||
- **`server-backup.service`**: `RequiresMountsFor=/mnt/nas/backups` toegevoegd (triggert cifs-automount bij timer-fire), `ReadWritePaths` uitgebreid met `/mnt/nas/backups` (`ProtectSystem=strict` blokkeert anders schrijven naar de NAS-repo), `Documentation=`-URL gecorrigeerd. De pre-existing `RuntimeMaxSec= has no effect with Type=oneshot`-warning is cosmetisch en niet aangepakt.
|
||
|
||
## Onderweg geleerde quirks
|
||
|
||
- **`sudo -E` werkt niet op deze sudoers** — geeft warning `preserving the entire environment is not supported`. Niet erg: de scripts sourcen het env-file binnen de sudo'd shell zelf (`sudo bash -c '. /etc/restic-backup.env; ...'`), dus `-E` is overbodig.
|
||
- **B2 401-error op `b2_list_buckets` was misleidend** — keys waren prima (`b2_authorize_account` werkte), het probleem was dat `RESTIC_REPO_B2` een andere bucket-naam had dan waar de key voor scoped is. B2 geeft dan 401 i.p.v. 403/404.
|
||
- **B2 cap-error verschijnt als `403: Cannot upload files, storage cap exceeded`** — niet 402/payment-related. Cap kan op nul staan voor accounts die nog nooit een bucket vol hadden; verhogen via *Account → Caps & Alerts → Storage Cap*.
|
||
- **47× dedup** op de eerste snapshot — vooral door de drie git-repos in `/srv/scrum4me/repos/` plus de ~12k worker-log files in `/srv/scrum4me/worker-logs/idea/runs/` met veel overlap.
|
||
|
||
## Eerste-run-cijfers
|
||
|
||
```
|
||
NAS: 16 GB op disk (du)
|
||
Restore-size: 974 GiB (over 2 snapshots; ~487 GiB per snap)
|
||
Raw-data: 20.6 GiB (post-dedup)
|
||
On-disk: 15.6 GiB (post-compressie, 1.32x)
|
||
Snapshots: 2 (eerste run + post-fix re-run)
|
||
|
||
B2: ~16 GB op disk (vergelijkbare dedup + compressie)
|
||
Snapshots: 1 (na cap-bump + script-fix)
|
||
Storage-kost: ≈ $0,10/maand bij huidige grootte
|
||
|
||
Eerste run (forgejo+B2 faalden): 47:42 wall-clock
|
||
Tweede run (alles success): ~15 min
|
||
Restore-test (na --include fix): 10s, 59 MiB gerestored
|
||
Files in snapshot: ~2.1M (1.9M unique blobs)
|
||
```
|
||
|
||
## Verificatie-status
|
||
|
||
| Plan-stap | Status |
|
||
|---|---|
|
||
| 1. Eerste run slaagt | ✓ (na 2 attempts; success in run 2 om 15:23) |
|
||
| 2. Snapshots zichtbaar | ✓ NAS×2, B2×1 |
|
||
| 3. Restore-test slaagt | ✓ 4/4 assertions ok in 10s |
|
||
| 4. Forgejo-restore-stack (F5) | ✗ niet uitgevoerd — separate vervolg |
|
||
| 5. Reboot-test | ✗ niet uitgevoerd — productie-reboot, los moment |
|
||
| 6. Failure-injectie | ✗ niet bewust uitgevoerd; we hebben **wel** organisch failure paths gezien (B2-cap, forgejo `--skip-db`) en die rapporteerden zoals verwacht (exit 75, juiste per-phase-status) |
|
||
| 7. Concurrency | ✗ niet getest — `flock`-pad zit in script |
|
||
| 8. Maandelijkse maintenance vanaf laptop | — over een maand, met dan-aan-te-maken maintenance-key |
|
||
|
||
## Te bewerken bestanden op `scrum4me-srv`
|
||
|
||
- `/etc/fstab` — extra cifs-regel voor `/mnt/nas/backups` (zie Deel A3).
|
||
- `/etc/restic-backup.env` — secrets, mode 0600 root:root.
|
||
- `/etc/restic-backup.password` — mode 0400 root:root, óók in passwordmanager.
|
||
- `/etc/systemd/system/server-backup.{service,timer}` — uit de repo's
|
||
`deploy/server-backup/`.
|
||
- `/srv/backups/{scripts,logs,status}/`, `/var/backups/databases/`.
|
||
- `/srv/scrum4me/ops-dashboard/deploy/server-backup/*` — code uit deze PR
|
||
(na `git pull` op de server).
|