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>
28 KiB
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_dumpallenforgejo dump+ apartepg_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.jsondie 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 jqvoor de eerste twee — de rest zit standaard). - De Scrum4Me-stack draait in Docker (
docker ps | grep scrum4me-postgres). /srv/scrum4me/compose/docker-compose.ymlbestaat (anders herzie je het exclude-pad inserver-backup.sh).- Tijd loopt synchroon (
timedatectl status) — backups gebruiken ISO-timestamps.
Voorwaarden (input van de gebruiker nodig)
- NAS-mount — pad zoals
/mnt/nas/backupsmet 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.passwordop 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.
-
Tools installeren
sudo apt update sudo apt install -y restic jq restic version -
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 -
NAS-mount aanmaken — een nieuwe mount op
/mnt/nas/backupsdie naar de subdirbackupsvan de bestaandessd-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,nofailzorgt dat de mount automatisch terugkomt bij reboot zónder de boot te laten hangen als de NAS even weg is. DeRequiresMountsFor=/mnt/nas/backupsinserver-backup.servicetriggert bovendien de automount voor de timer-run. -
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.passwordKopieer 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).
-
Bucket aanmaken in de Backblaze-UI of via
b2CLI:- 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
bypassGovernancekan 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).
- Naam:
-
Server-key aanmaken (gaat naar
/etc/restic-backup.envop de server):# via b2 CLI: b2 application-key create \ --bucket scrum4me-srv-backup \ --name-prefix scrum4me-srv \ server-backup-key \ listBuckets,listFiles,readFiles,writeFilesBewaar de output (
keyID+applicationKey). Verifieer in de UI dat de key nietdeleteFiles, nietdeleteKeys, nietbypassGovernanceheeft. -
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,bypassGovernanceDeze key komt nooit op de server. Gebruik alleen voor
restic forget --prunevanaf je laptop (zie Deel H). -
/etc/restic-backup.envaanmakensudo 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.envVul 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
-
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 ' -
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 ' -
Retentie droogtest — controleer dat het forget-beleid niet té agressief is op een eerste-snapshot-only repo. (Op een verse repo verwijdert
forgetniets, 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
-
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 -
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 -
Timer verifiëren
systemctl list-timers | grep server-backupToont next-run morgen 03:30 (+ randomized delay tot 10 min).
Deel E — Eerste run handmatig + statusfile-verificatie
-
Trigger
sudo systemctl start server-backup.service -
Live volgen
journalctl -u server-backup.service -fVerwacht: 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. -
Statusfile
sudo jq . /srv/backups/status/last-run.jsonVerwacht:
overall_status: "success", alle 5 verplichte fasensuccess(Forgejo magskippedzijn als die nog niet geconfigureerd is). -
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-srven tagsscheduled.
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=postgresmetNAME=forgejo⇒ zetFORGEJO_DB_NAME=forgejo, en als de Postgres-container nietscrum4me-postgresis:FORGEJO_DB_CONTAINER=....DB_TYPE=sqlite⇒ laatFORGEJO_DB_NAME=leeg; SQLite-DB komt mee inforgejo dump.
F4. Dump-strategie
Het script doet drie dingen voor Forgejo:
forgejo dump --skip-db -c <config> --type zip -f -— codebases, attachments, hooks, LFS metadata, etc.- Separate
pg_dump <forgejo_db>— autoritatieve DB-restore-bron (Forgejo docs documenteren bekende import-issues bij DB-inhoud uitforgejo dump, daarom--skip-db). - 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.
-
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 -
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 -
Daadwerkelijke prune (vereist
bypassGovernancecapability — alleen via maintenance-key):restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune -
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).
-
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(uitdeploy/server-backup/)./srv/backups/scripts/restore-test.sh(idem)./etc/systemd/system/server-backup.service,server-backup.timer(uitdeploy/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)
- Eerste run slaagt — Deel E groen, statusfile
overall_status: success. - Snapshots zichtbaar op beide repos via
restic snapshots. - Restore-test slaagt —
restore-test.sh nas→overall_status: successin/srv/backups/status/last-restore-test.json, alle assertionsok. - Forgejo-restore-stack (F5) —
forgejo doctor check --allrond zonder errors,/api/v1/versionantwoordt. - Reboot-test — server reboot,
systemctl list-timerstoontserver-backup.timermet next-run gepland; NAS-mount automatisch terug. - 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.
- NAS unmount → script eindigt met
- Concurrency — tweede
systemctl start server-backup.servicetijdens lopende run → exit 75, log toontanother server-backup is already running. - Maandelijkse maintenance — eerst keer succesvol uitgevoerd vanaf laptop, B2
forget --pruneslaagt 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-dbweggehaald uitdump_forgejo. Forgejo 11.x heeft die flag niet (verwijderd na de Gitea-fork). Output vanforgejo 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 metforgejo_db_dump-fase, maar onschuldig.server-backup.sh: subshell-bug indetermine_exit_code— werd aangeroepen viaEXIT_CODE=$(determine_exit_code), dusOVERALL_STATUS=...werd in de subshell gezet en lekte niet naar de parent. Resultaat:last-run.jsonschreef altijdoverall_status: "unknown"en de eind-banner idem. Fix: directe call die zowelOVERALL_STATUSalsEXIT_CODEin 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--includeop 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/backupstoegevoegd (triggert cifs-automount bij timer-fire),ReadWritePathsuitgebreid met/mnt/nas/backups(ProtectSystem=strictblokkeert anders schrijven naar de NAS-repo),Documentation=-URL gecorrigeerd. De pre-existingRuntimeMaxSec= has no effect with Type=oneshot-warning is cosmetisch en niet aangepakt.
Onderweg geleerde quirks
sudo -Ewerkt niet op deze sudoers — geeft warningpreserving 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-Eis overbodig.- B2 401-error op
b2_list_bucketswas misleidend — keys waren prima (b2_authorize_accountwerkte), het probleem was datRESTIC_REPO_B2een 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'sdeploy/server-backup/./srv/backups/{scripts,logs,status}/,/var/backups/databases/./srv/scrum4me/ops-dashboard/deploy/server-backup/*— code uit deze PR (nagit pullop de server).