# 46. The database on an emptyDir, replicated with Litestream Date: 2026-11-05 ## Status Accepted. Opt-in (`litestream.enabled`, off by default); the DLS installer turns it on. With it off the chart renders exactly as before. ## Context scrap keeps SQLite in WAL mode. On DLS's argus cluster the only dynamic storage is `netapp`, which is NFS, and the [storage notes](../../how-to/deploy-kubernetes.md) say never to put the database on NFS: WAL needs shared memory and locking a network filesystem does not provide reliably. The owner decided that the database moves off NFS rather than testing whether it copes there. This ADR does not claim to explain any past hang: none was traced to NFS. Block storage, which the storage notes recommend, is not an option here: the argus admins removed it a while back for security reasons, and `netapp` is the only dynamic class, so it was not pursued. ## Decision **The shape.** The database is at `/db/scrap.db` (`SCRAP_DB_PATH`) on an emptyDir on the node's disk (not `medium: Memory`; argus's is a local xfs, seen on the cluster). A Litestream sidecar replicates it to `/data/litestream` on the data volume. `files/` and the preview cache stay on the data volume. With `SCRAP_DB_PATH` unset nothing changes: `Store` builds `dir / "scrap.db"`, as before. The lock, WAL and `scrap restore`'s `scrap.db.restore` follow the database, so its rename never crosses filesystems. `gc.lock` stays in the data folder, on NFS, as it does today. **The version: Litestream 0.5.18, by tag and digest.** The 0.5 line is the maintained one (the docs call 0.3.14 "previous"); its restore checks LTX files and fails on a damaged one, seen below, and it has what is used here: `shutdown-sync-timeout`, `snapshot`, and a file replica whose renames are fsynced (0.5.16). 0.5.18 rather than the two-month-old 0.5.17, despite being two days old when chosen, for its fixes on this path: "read the whole WAL when the last-page check fails", "keep read lock alive after caller context is cancelled" and "cache LTX pages for checkpoint verification"; and it is the version the trial below ran. Moving it is a decision: read the release notes, and repeat the trial's restore steps. **Two images.** The sidecar is Litestream's own image, not a binary added to scrap's, so nobody with `litestream.enabled` off carries it. An init container copies its static binary into a small emptyDir, so the Python below can call it. The Debian image (`0.5.18`, not `-scratch`) has the `cp` and `sh` this needs. The chart still makes no ConfigMap: the sidecar writes its config at start. **Starting up: `scrap db-init`, in an init container, so the logic can be unit tested.** The first that applies decides: 1. A database on the emptyDir is kept (the pod restarted on its node), never overwritten. 2. A staged restore is taken up (below), and the replica renamed aside. 3. A replica is restored. No `-if-replica-exists`: if the restore or the integrity and schema check fails, the init container fails and scrap does not start. A retry can work. 4. No replica but a `/data/scrap.db` with pages (an install from before this; an empty one counts as absent): brought across once with SQLite's backup API (it may have a WAL), checked, left where it is. 5. Neither: a new database, unless something says there should be one. **Telling "really new" from "looks empty because something is wrong".** A new volume with no history looks exactly like a replaced or mistakenly empty one from inside the volume, so the evidence has to include something that outlives it. `db-init` fails closed on any of: - `.scrap-db-state.json` on the data volume, written the first time a database is put in place (`how` and `at`; a later restore of the replica leaves those and sets `replica_restored_at`), with no replica beside it. So the replica cannot vanish unnoticed, and a stale `/data/scrap.db` cannot come back by rule 4. This also fires if the pod dies between the first placement and Litestream's first write, about a second; it costs a person a look, not data. - Files in `files/`. - Backup runs (daily or monthly) in the backup folder, which is on other storage (at DLS, `/dls/science`): the one signal that notices a new, empty data volume. And any error looking is a failure, not "absent": `os.stat`, not `Path.exists()`, which from Python 3.12 answers `False` on a stale NFS handle. It cannot catch everything: a data volume and backup folder both new and empty really is a new install, and with backups off only the first two apply. `litestream.init.override` gets past these refusals, for after a person has looked. Each use is recorded (`override_at`), and gc only counts until a person clears it (`scrap db-hold --clear`), so leaving it on cannot make a later loss of the replica silent. A source over half of `litestream.sizeLimit` is refused regardless, as the WAL and Litestream's own LTX share that emptyDir. **Off Litestream.** With the setting off, or a `helm rollback` to a chart that has it off, `/data/scrap.db` is the copy from before, and gc would delete files only the newer database uses. So while the state file exists and `SCRAP_DB_PATH` is unset, the app and `scrap gc` refuse. A plain `scrap restore` removes the state file: that is the way off. Installs that never used Litestream have no state file and are not affected. A rollback to a chart from before this code cannot be guarded, and the how-to warns of it. **Restoring a backup.** The old steps wrote `/data/scrap.db`, which the replica would then silently win over. So `scrap restore --stage` leaves the checked, migrated backup at `/data/scrap.db.restored`, which outranks the replica; the replica is renamed `litestream.retired-