Ops-dashboard/docs/runbooks/server-backup.md
Janpeter Visser 20de584759 fix(server-backup): host-paths + script bugs uit eerste install
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>
2026-05-15 16:34:21 +02:00

562 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).