docs(wiki): record backup deploy gotchas (compose allowlist + host mount)
CI / check (push) Successful in 40s
CI / check (push) Successful in 40s
Two lessons from the first park-buzi staging deploy, both in backup-recovery.md: - A new server env var (BACKUP_KEY) must be added to docker-compose.yml's server.environment: allowlist, not just the Komodo secret/Stack env — otherwise the container never receives it (inspect shows it absent, not empty). - The backup target must be a host path bind-mounted into the container; a desktop- automounted USB (/run/media/...) is invisible inside the container, so Test target reports 'does not exist'. Destinations are admin-provisioned (fstab + compose bind- mount), not operator-pluggable — partly a threat-model feature. Acknowledged as a flexibility limitation; USB-automount-to-container flow deferred. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -99,6 +99,38 @@ All three supported in the first cut; the manual button and the periodic timer s
|
|||||||
- **SFTP** — push to an SFTP endpoint, useful for an offsite copy. **FTP is excluded** (plaintext
|
- **SFTP** — push to an SFTP endpoint, useful for an offsite copy. **FTP is excluded** (plaintext
|
||||||
credentials + data); SFTP is the safe equivalent.
|
credentials + data); SFTP is the safe equivalent.
|
||||||
|
|
||||||
|
### The target must be a bind-mounted host path — NOT a casually-plugged USB (2026-06-29)
|
||||||
|
|
||||||
|
The server runs **inside the `parking-server` container**, so it can only `stat()`/write paths that
|
||||||
|
are **bind-mounted into that container**. A USB stick the operator plugs in lands at a desktop
|
||||||
|
auto-mount path on the *host* (`/run/media/<user>/<UUID>`), which **does not exist inside the
|
||||||
|
container** — so the in-UI **Test target** correctly reports *"location does not exist."* This bit on
|
||||||
|
the first booth deploy (2026-06-29): `BACKUP_KEY` was finally injected, then the target test failed
|
||||||
|
because the USB path wasn't visible to the process.
|
||||||
|
|
||||||
|
**So a backup destination is provisioned by the ADMIN at the host level, not chosen ad hoc by the
|
||||||
|
operator.** The procedure:
|
||||||
|
|
||||||
|
1. Attach the disk (external HDD/SSD/USB) and mount it at a **stable host path** (e.g. `/mnt/backup`)
|
||||||
|
via **`/etc/fstab` by UUID** — *not* the desktop automounter, whose UUID-named path changes per
|
||||||
|
drive and vanishes on unplug.
|
||||||
|
2. **Bind-mount that host path into the container** in the prod compose (e.g.
|
||||||
|
`/mnt/backup:/mnt/backup` on the `server` service — same pattern as the `/dev/usb` printer
|
||||||
|
passthrough in [[container-deployment]]).
|
||||||
|
3. In the UI (Setup → Backup), set the **target directory to the in-container path** (`/mnt/backup`)
|
||||||
|
and **Test target** — now writable.
|
||||||
|
|
||||||
|
> **This is partly a feature, not just a limitation** ([[threat-model]]): because the destination is
|
||||||
|
> a host-provisioned bind-mount, the **booth operator cannot redirect backups to a removable stick
|
||||||
|
> they walk off with** — real destinations are an admin/host decision, on the trusted side of the
|
||||||
|
> [[trust-boundary]]. A network share (SMB/NFS) is the same shape: mount on the host, bind-mount in.
|
||||||
|
>
|
||||||
|
> **Limitation acknowledged:** the backup target is therefore **not operator-flexible** — you cannot
|
||||||
|
> just plug in a USB and back up from the UI. Adding a new destination = a host `fstab` + compose
|
||||||
|
> bind-mount change + redeploy. For the appliance model (single-purpose, admin-provisioned) this is
|
||||||
|
> the right trade; a future "back up to a freshly-plugged removable drive" flow would need host-level
|
||||||
|
> automount detection wired to the container, which is **deferred / not built**.
|
||||||
|
|
||||||
## Retention at the destination
|
## Retention at the destination
|
||||||
|
|
||||||
**Keep last N + thinned dailies** (e.g. last 7 daily / last 4 weekly) — bounded disk use, and it
|
**Keep last N + thinned dailies** (e.g. last 7 daily / last 4 weekly) — bounded disk use, and it
|
||||||
@@ -155,6 +187,18 @@ timer + the manual route**. What landed:
|
|||||||
- **Komodo wiring.** `BACKUP_KEY` is a **per-booth Komodo secret** (`[[park_buzi_backup_key]]` in
|
- **Komodo wiring.** `BACKUP_KEY` is a **per-booth Komodo secret** (`[[park_buzi_backup_key]]` in
|
||||||
`komodo/resources.toml`; documented in `komodo/.env.komodo.example`), escrowed offsite alongside
|
`komodo/resources.toml`; documented in `komodo/.env.komodo.example`), escrowed offsite alongside
|
||||||
`EVENT_SIGNING_KEY`. It is the *only* backup env var — target + retention are in the DB.
|
`EVENT_SIGNING_KEY`. It is the *only* backup env var — target + retention are in the DB.
|
||||||
|
|
||||||
|
> **Gotcha — compose `environment:` is an ALLOWLIST (cost a full booth-deploy session, 2026-06-29).**
|
||||||
|
> Wiring `BACKUP_KEY` as a Komodo secret + Stack-env line is **necessary but not sufficient**:
|
||||||
|
> `docker-compose.yml`'s `server.environment:` block only forwards the variables it *names*. The key
|
||||||
|
> was wired everywhere (secret store, Stack env, `.env.example`, schema) but **never added to that
|
||||||
|
> compose block**, so the container came up *without* it — `docker inspect ...Config.Env` showed
|
||||||
|
> `JWT_SECRET`/`EVENT_SIGNING_KEY` present and `BACKUP_KEY` **absent (not empty)**, while the Backup
|
||||||
|
> screen correctly reported "BACKUP_KEY missing". Diagnosis was muddied by chasing Komodo (secret
|
||||||
|
> name, re-sync, destroy/redeploy, env-only-change-doesn't-recreate) before checking the compose
|
||||||
|
> allowlist. **Lesson: a new server env var needs a line in `docker-compose.yml` `server.environment:`
|
||||||
|
> too — that's the only place env reaches the container.** Fixed: `BACKUP_KEY: ${BACKUP_KEY:-}` next to
|
||||||
|
> `EVENT_SIGNING_KEY`. Quick check on a booth: `docker inspect <server> --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i backup`.
|
||||||
- **`server.ts`** — an **unref'd daily timer** (`backupService.runScheduled`), a **no-op until
|
- **`server.ts`** — an **unref'd daily timer** (`backupService.runScheduled`), a **no-op until
|
||||||
configured**, and **deliberately NOT run at startup** (a just-power-cut booth shouldn't write to a
|
configured**, and **deliberately NOT run at startup** (a just-power-cut booth shouldn't write to a
|
||||||
possibly-unmounted disk; the daily cadence + the manual button cover it).
|
possibly-unmounted disk; the daily cadence + the manual button cover it).
|
||||||
|
|||||||
+17
@@ -1976,3 +1976,20 @@ TAG=stage-84f00db; komodo/README.md promotion section + per-booth secret list no
|
|||||||
fleet-deployment-komodo open-item resolved + new 'Promotion tiers' table; container-deployment tag list +
|
fleet-deployment-komodo open-item resolved + new 'Promotion tiers' table; container-deployment tag list +
|
||||||
:stage. Per-booth secrets (jwt/event_signing/backup) must pre-exist in Core for park-buzi; migrations run at
|
:stage. Per-booth secrets (jwt/event_signing/backup) must pre-exist in Core for park-buzi; migrations run at
|
||||||
boot so a promotion auto-migrates the staging ledger (where a bad migration is caught before prod).
|
boot so a promotion auto-migrates the staging ledger (where a bad migration is caught before prod).
|
||||||
|
|
||||||
|
## [2026-06-29] fix+doc | First park-buzi backup deploy: BACKUP_KEY allowlist + container mount constraint
|
||||||
|
First real-world staging deploy surfaced two backup gotchas, both now in [[backup-recovery]]:
|
||||||
|
(1) BACKUP_KEY was wired as a Komodo secret + Stack-env line but NEVER added to docker-compose.yml's
|
||||||
|
server `environment:` ALLOWLIST — so the container came up without it (docker inspect: JWT/SIGN present,
|
||||||
|
BACKUP_KEY absent-not-empty; Backup screen "BACKUP_KEY missing"). A whole session was lost chasing Komodo
|
||||||
|
(secret name, re-sync, destroy/redeploy, env-only-change-doesn't-force-recreate) before checking the
|
||||||
|
compose allowlist. Fix: `BACKUP_KEY: ${BACKUP_KEY:-}` next to EVENT_SIGNING_KEY (commit on dev 8f32d90,
|
||||||
|
promoted dev→stage merge d0b609e → built stage-d0b609e). Lesson recorded: a new server env var ALSO needs a
|
||||||
|
line in the compose environment block.
|
||||||
|
(2) The backup target must be a HOST path BIND-MOUNTED into the container — a casually-plugged USB at
|
||||||
|
/run/media/<user>/<UUID> is invisible inside the container, so Test target rightly says "does not exist".
|
||||||
|
Provisioning = fstab-by-UUID a stable host path (e.g. /mnt/backup) + bind-mount it in prod compose + set
|
||||||
|
the in-container path as the UI target. Acknowledged limitation: backups are NOT operator-flexible (no
|
||||||
|
plug-a-USB-and-go); adding a destination is an admin host+compose change. Partly a feature vs the
|
||||||
|
operator-adversary threat model (operator can't redirect backups to a removable stick). USB-automount-to-
|
||||||
|
container flow deferred/not built.
|
||||||
|
|||||||
Reference in New Issue
Block a user