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

28 KiB
Raw Blame History

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-wachtwoordopenssl 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

    sudo apt update
    sudo apt install -y restic jq
    restic version
    
  2. Directories aanmaken

    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.

    # 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

    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):

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

    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

    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

    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

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

    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

    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

    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

    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

    sudo systemctl start server-backup.service
    
  2. Live volgen

    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

    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

    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

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

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

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.

# 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

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

# 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 sectie 2a primair.

G3. Forgejo herstellen

Volg F5 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):

    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):

    restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --dry-run
    
  3. Daadwerkelijke prune (vereist bypassGovernance capability — alleen via maintenance-key):

    restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
    
  4. Diepere check:

    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 slaagtrestore-test.sh nasoverall_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).