Ops-dashboard/docs/runbooks/server-backup.md
Madhura68 ab87c0fada feat(server-backup): restic dual-repo backup (NAS + B2) with dashboard UI
Adds a server-wide backup capability beyond the existing ops_dashboard
pg_dump flow:

- Daily systemd timer (03:30) runs pg_dumpall + Forgejo dump, then restic
  to a local NAS repo and an offsite Backblaze B2 repo with Object Lock.
  Phase-based script with single-instance flock, structured statusfile,
  systemd hardening, and live-datadir excludes (Postgres / Forgejo) so
  the dumps stay authoritative.
- Ops-agent gets nine new read-only/trigger commands (snapshots, stats,
  status, logs, plus two triggers) backed by sudoers-whitelisted wrapper
  scripts that source /etc/restic-backup.env so the agent never sees the
  restic password or B2 keys.
- Two new flows (server_backup_full, server_backup_restore_test) drive
  the dashboard's "Backup now" and "Restore test" buttons.
- /settings/backups gains a Server backup section with overall + per-phase
  status, NAS / B2 snapshot tables, restore-size / raw-data / dedup-ratio
  stats, and the last restore-test result. The existing pg_dump section
  is preserved unchanged.
- Runbook docs/runbooks/server-backup.md follows the tailscale-setup
  pattern (plan + addendum) and covers B2 Object Lock + scoped keys,
  Forgejo subplan with isolated restore-test stack, the off-server
  maintenance flow for B2 prune, and the integrity-check schedule.

Code-only change — installation on scrum4me-srv follows the runbook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:03:00 +02:00

462 lines
20 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/backup-server` 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 controleren / aanmaken**
```bash
mountpoint -q /mnt/backup-server && echo "OK" || echo "NIET gemount"
```
Zo nee: `fstab`-regel toevoegen, `systemctl daemon-reload`, `mount -a`. Zorg dat de mount automatisch terugkomt bij reboot — anders crashed de eerste backup-run na een reboot.
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/ops/repos/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/ops/repos/ops-dashboard/deploy/server-backup/server-backup.sh /srv/backups/scripts/
sudo cp /srv/ops/repos/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/ops/repos/ops-dashboard/deploy/server-backup/server-backup.service /etc/systemd/system/
sudo cp /srv/ops/repos/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 <forgejo> --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ println }}{{ end }}'
docker exec <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 <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/backup-server` — 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
> Vul deze sectie na de eerste uitvoering met alle afwijkingen van het plan
> hierboven: exacte Forgejo container-naam, image-versie, eventuele paden die
> anders bleken, sudoers-precieze regels, Object Lock-retention die je gekozen
> hebt, B2 key-IDs (geredacteerd), tijden van eerste runs, etc. Zelfde
> discipline als [tailscale-setup.md](tailscale-setup.md).