# Backup and Recover Schema (svc-bkp-* role family)
End-to-end flow of the backup chain and its recover counterparts. Solid
arrows run on schedule (systemd units + pull); dashed arrows are the
recover direction (`files/python/recover.py` of each role, enforced by
`tests/lint/ansible/roles/test_bkp_roles_have_recover.py`). The swarm
test pipeline exercises the full chain in
`scripts/tests/deploy/swarm/routine/backup/base.sh`.
```mermaid
flowchart TB
subgraph source["Source host (e.g. arch-server1 / swarm manager + NFS server)"]
direction LR
volumes["Docker volumes + databases"]
export["NFS export
<export-base>/state/<volume>"]
secrets["Host secrets + CA + ACME + node identity
/var/lib/infinito/secrets, /etc/<domain>/ca,
/etc/letsencrypt, /etc/certbot, ssh host keys, machine-id"]
unit_vol["svc-bkp-volume-2-local.service
(baudolo, nightly)"]
unit_nfs["svc-bkp-nfs-2-local.service
(rsync --link-dest, nightly)"]
unit_sec["svc-bkp-secrets-2-local.service
(rsync --link-dest per subtree, nightly)"]
src_backups["/var/lib/infinito/backup
<hash>/backup-docker-to-local/<gen>/<volume>/files
<hash>/backup-nfs-to-local/<gen>/files
<hash>/backup-secrets-to-local/<gen>/files/<subtree>"]
volumes -->|"stop-aware dump + rsync"| unit_vol --> src_backups
export -->|"differential snapshot"| unit_nfs --> src_backups
secrets -->|"differential snapshot"| unit_sec --> src_backups
end
subgraph backuphost["Backup host (e.g. arch-server2)"]
direction LR
unit_pull["svc-bkp-remote-2-local.service
(pull_specific_host.py, backup@source,
forced-command ssh wrapper)"]
bkp_backups["/var/lib/infinito/backup
(pulled generations = recovery store)"]
unit_dev["svc-bkp-local-2-device.service
(script.py, plug-triggered via .mount)"]
unit_pull --> bkp_backups
bkp_backups --> unit_dev
end
subgraph device["Encrypted device (LUKS)"]
stick["<mount>/<hash>/svc-bkp-local-2-device/<ts>/backup"]
end
source --> backuphost --> device
src_backups -->|"rsync over ssh (pull only)"| unit_pull
unit_dev -->|"rsync --link-dest"| stick
stick -.->|"local-2-device recover.py
luksOpen + mount + newest snapshot
rsync --delete"| bkp_backups
bkp_backups -.->|"operator rsync
(backup user is pull-only)"| src_backups
src_backups -.->|"nfs-2-local recover.py
backup unit first, then rsync --delete"| export
src_backups -.->|"volume-2-local recover.py
backup unit first, then rsync --delete
into the volume mountpoint"| volumes
src_backups -.->|"volume-2-local recover.py --databases
baudolo-restore <engine> --empty per dump,
consumers must be down"| volumes
src_backups -.->|"secrets-2-local recover.py
backup unit first, then rsync --delete
per subtree to its fixed system path"| secrets
```
Databases travel as sql dumps, not as a file tree (`--dump-only-sql`), so
they have their own recover leg: `recover database `, which
`recover full` plans right after the volume steps. That leg is the one part
of the chain that cannot run on a bare host: a dump is replayed through
`docker exec`, so the database service has to be deployed and up, with its
consumers stopped. The engine of a dump is
resolved from the consuming role's `meta/services.yml` service key in
`utils/recovery/databases.py` — the same value the backup seed used — and a
replay refuses to run while a consumer of that database is up, because
`--empty` pre-cleans the schema and a booting consumer would recreate it
underneath the replay.
Per-role recover procedures live in each role's `## Recover` README
section; `svc-bkp-remote-2-local` documents its one-way opt-out in
`files/python/recover.py.nocheck`.
## Deploy-time trigger flow
When the backup units actually run during a deploy, keyed on `MODE_BACKUP`
and whether this is a first install or an update. The nightly systemd
timers fire independently of every path below.
```mermaid
flowchart TB
start["Deploy starts (tasks/stages/01_constructor.yml)"] --> boot["Pre-preload bootstrap:
docker + NFS export + token store ready"]
boot --> mode{"MODE_BACKUP?"}
mode -->|true| pre["Preload pass instant-starts the pre-state snapshot
(force_flush_instant + state started):
svc-bkp-secrets, svc-bkp-volume (pulls sys-svc-container for a
live dockerd + ensures /opt/compose first), and svc-bkp-nfs
after svc-storage-nfs-server (export must exist)"]
pre --> kind{"install or update?"}
kind -->|"1st install"| baseline["Source still empty (no volumes/export yet)
-> near-empty baseline generation"]
kind -->|update| anchor["Captures the live pre-update state
-> rollback anchor before any mutation"]
baseline --> apps
anchor --> apps
apps["App pass deploys / updates the stacks"] --> term["Terminator (end of play): svc-bkp-remote-2-local
forced pull (force_flush_final)"]
term --> done["Deploy done"]
mode -->|false| install_only["Units installed + enabled, never started here"]
install_only --> apps_nb["App pass deploys / updates the stacks"] --> done
done --> timers["Nightly timers keep running on schedule
(both MODE_BACKUP paths)"]
```
A failing pre-state snapshot aborts the deploy (no `suppress_flush`):
no update proceeds without a successful backup of the state it is about
to change. `svc-bkp-local-2-device` is never started at deploy time (no
device present); it stays plug-triggered via its `.mount` unit.