Files
FabledSteward/docs/core/backups.md
T
bvandeusenandClaude Opus 5.5 8feec4dc77
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m25s
CI / integration (push) Successful in 1m31s
CI / publish (push) Successful in 1m13s
feat(mcp): Steward's own backups: the runs, the next one, checking and making one (#5836)
list_backups, check_backup and make_backup are Settings → Backups for an
agent, admin only like the page. make_backup runs the page's job in the
background and is audited as "<user> via MCP". Restoring stays in the
browser. get_overview names a scheduled backup that failed, is two hours
past its slot, or has no bucket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:09:12 -04:00

221 lines
9.7 KiB
Markdown

# Backups: Steward backing itself up, and coming back
Steward sends a backup of itself to an S3 bucket once a day. Fabled Archives
is the intended target. A backup is one file. Settings → Backups restores it
on a running Steward, first-run setup restores it on a new one, and
`steward restore` brings Steward back from it when Steward can't start.
## What a backup holds
`steward-<UTC time>.tar`, containing:
| Member | What it is |
|---|---|
| `steward.dump` | `pg_dump --format=custom` of Steward's database |
| `secret.key` | The key Steward's stored passwords and tokens are sealed with |
| `manifest.json` | Format, Steward version, migration heads, time, and the tables whose rows were left out |
A backup is for **system recovery**: it holds what you would otherwise have
to set up again, and no logs or history.
**Kept:**
- settings, with the passwords and keys they hold;
- users and API tokens;
- hosts and their agent enrolments, so agents don't need enrolling again;
- monitors, alert rules, maintenance windows and maintenance cadences;
- Ansible inventory and schedules;
- dashboards and share links;
- SNMP devices, Prometheus targets and widget templates, UniFi client names.
**Left out** (the tables come back empty and fill again as Steward runs):
- the audit log;
- alert history and alert state. Each rule starts inactive and is watched
again within a minute;
- metric anomalies and their reviews;
- metrics, their rollups and learned normals;
- monitor check results;
- Docker, Traefik and UniFi history;
- Ansible run records and maintenance job history;
- the email queue, pending invites and password-reset links.
The list is `steward/backup/contents.py`. A test fails when a new table
appears in neither list, so whoever adds a table decides.
**The secret key is in the file.** That makes a restore complete on its own.
It also means anyone holding a copy can read every password and key Steward
keeps. Keep the bucket private.
## Setting it up with Fabled Archives
1. In Archives, make a backup target for Steward with an S3 key. The default
append-only key is enough: Steward only uploads and never deletes.
How long backups are kept is set on the Archives side.
2. In Steward, open **Settings → Backups** and fill in:
- **Endpoint**: the S3 address Archives shows for the target, without the
bucket (for example `http://archives:8081`). Requests are path-style:
`endpoint/bucket/file`.
- **Bucket**: the target's bucket name.
- **Access key** and **Secret key**: from Archives. The secret is sealed at
rest; a blank box on save keeps the stored one.
3. Press **Test the bucket**. A key that may not list ("AccessDenied") still
counts as reached: an append-only key can't list, and uploads work anyway.
4. Tick **Back up every day**, pick the hour (on your clock), and save.
**Back up now** makes one straight away.
A day missed while Steward was down is made up when it comes back. A failed
backup is tried again an hour later, never sooner. The last 30 runs are
listed on the page, and each one is in the audit log (`backup.made` /
`backup.failed`).
### Alert when backups stop
While backups are on, Steward records two metrics under the source
**Steward itself**, resource `backup`:
| Metric | Meaning |
|---|---|
| `backup_age_secs` ("Newest backup's age") | Seconds since the last backup was sent, updated every 15 minutes |
| `backup_bytes` ("Backup size") | The size of the last backup sent |
A rule such as `backup_age_secs > 93600` (26 hours) fires when a daily backup
has been missed.
An agent on the MCP sees the same without a rule. `get_overview` names a
scheduled backup that failed, is two hours past its slot with none made, or has
no bucket set up. `list_backups`, `check_backup` and `make_backup` are this
page's admin tools (see [MCP](mcp.md)); restoring isn't among them.
## Checking a backup
**Settings → Backups → Check** compares a backup with what is in place, without
changing anything. It shows what a restore would bring back, change or remove.
Each row under Recent backups has its own Check button, and **Check a file**
takes a file downloaded from the bucket. From a shell:
```sh
docker exec -u app <steward container> steward check-backup /tmp/steward-20261015T030000Z.tar
```
The check reports:
- for each area (Settings, Users and API tokens, Hosts and agents, Monitors,
Alert rules, Ansible, Dashboards, Plugins), how many entries are the same,
differ, are only in the backup or are only here;
- each setting that differs, with both values. A password or key shows only
"differs": sealed values are opened with their own key and compared, never
shown;
- whether the backup's secret key is the one in use, and whether it opens the
backup's own sealed values. A backup whose key can't open them fails the
check, because its secrets would not come back;
- whether the backup was made by this Steward's schema, an older one or a
newer one.
What Steward records as it runs is left out of the comparison: when an agent
or monitor was last seen, a schedule's last run, the backup history. These
change by the minute. The list is `ACTIVITY` and `STATE_SETTINGS` in
`steward/backup/contents.py`.
Each backup Steward sends is checked once it is written, and its row under
Recent backups says so. A backup is sent even when its check can't run.
Checking a backup on the bucket reads it back from there. An append-only key
may not be allowed to; the page then says AccessDenied, and the file can be
downloaded and checked with **Check a file** instead.
## Making a backup by hand
```sh
docker exec -u app <steward container> steward backup --to /tmp/backups
```
This writes the file and sends it nowhere. It needs the same database access
as Steward itself.
## Restoring
### From the web UI
**On a running Steward: Settings → Backups.** Press **Restore** on one of
the recent backups, or choose a file on the "Check or restore a file" card.
Steward first shows what the backup would change: the same comparison as
Check. Then it says what a restore does and asks you to type `restore`.
- **A backup of this Steward's schema goes back in place.** Settings,
users, hosts, monitors, alert rules and dashboards become the backup's.
The audit log and the history of everything still here stay. History of
anything the backup doesn't have goes with it. It is one transaction: if
anything fails part-way, nothing changes, and Backups says why.
- **A backup from an older Steward replaces everything**, history included,
and you tick "Replace everything" as well. Steward brings it up to date
as it starts.
- **A backup from a newer Steward isn't offered.** Update Steward first.
Before anything changes, Steward makes a **safety backup** of itself as it
stands. It goes to `/data/backups` (the newest five are kept) and to the
bucket, and it is listed under Recent backups as "before restore". A
restore can be undone by restoring that.
While the restore runs:
- every page says Steward is restoring;
- agents get a 503 and keep their readings until Steward is back;
- scheduled work holds.
Then Steward restarts (it exits with code 3 so Swarm, compose or systemd
starts it again), and you sign in as a user from the backup. If an older
backup's full replace fails after the database was emptied, Steward starts
at first-run setup, where the safety backup can be uploaded.
**On a new Steward: first-run setup.** Next to "Create the initial admin
account" is **Or restore from a backup**: upload a backup file instead,
and Steward comes up with its users, settings and key. There is nothing to
keep yet, so no safety backup is made.
The backup's secret key is put in place as `/data/secret.key`. If
`STEWARD_SECRET_KEY` is set instead, it must equal the backup's key, or the
restore stops before anything changes.
### From the command line
`steward restore` writes into an **empty** database. Pass `--wipe` to drop
everything in the database's public schema first. Use it when Steward
can't start at all.
`steward restore --in-place` does what Settings → Backups does for a backup
of the same schema, from the shell: it replaces what the backup carries and
keeps the audit log and history, in one transaction. Restart Steward after
it so the restored settings load.
1. Stop Steward (scale its service to 0) so nothing writes during the restore.
2. Download the backup file from Archives.
3. Run the restore with the image, pointed at the database:
```sh
docker run --rm \
-e STEWARD_DATABASE_URL=postgresql+asyncpg://steward:…@db:5432/steward \
-v steward_data:/data -v "$PWD":/restore \
git.fabledsword.com/bvandeusen/steward:latest \
steward restore /restore/steward-20261012T030000Z.tar
```
- The backup's secret key is written to `/data/secret.key`.
- If `STEWARD_SECRET_KEY` is set instead, it must equal the backup's key,
or the restore stops and says so. Read the key with
`tar -xOf <file> secret.key`.
4. Start Steward. On start it runs any migrations the backup is behind on.
**How the restore works.** pg_restore runs in its three sections: schema,
then data, then constraints and indexes. Between the data and the
constraints, a kept row that points at a left-out row is put right by its
foreign key's ON DELETE:
- a SET NULL link is cleared and the row kept;
- a row whose foreign key would cascade or block is removed.
pg_restore 17 writes each section as a script for psql. It runs against
Postgres 16 with one setting line dropped (`transaction_timeout`, which 16
doesn't know).
CI checks the whole path on every build. The built image backs up its own
database and checks the backup against it. It changes a setting and puts
the backup back in place: the setting returns and the audit log stays.
Then it restores into a second, empty database, checks a setting came back
and the metrics table and audit log are empty, and starts Steward on it.