Skip to content
Lumaft documentation contents
Lumaft documentation

Backup and disaster recovery

What to back up, how, how often, what a restore gives back, and what it does not.

Backup and disaster recovery

The database is the product. Everything Lumaft has ever observed or been told is in it, and nothing else can regenerate the parts your backends have since pruned. This page is the operating procedure for keeping it.

What a restore gives back, and what it does not

Restoring a backup rewinds Lumaft's own record to the moment the backup was taken.

  • Current projections converge forward. Backends are re-read on the normal cadence, so Stacks, resources, outputs, and locks are current again within minutes.
  • Durable history from before the backup is whatever the backup held. Operations Lumaft accepted between the backup and the failure are gone. If the backend still holds them, the next update-history cycle re-observes them as fresh evidence; if the backend has pruned them, they are lost.
  • Accounts, tokens, retention policy, and audit rows are restored as they were. Sessions are not; everyone signs in again.
  • Transcript and state bodies decrypt only with the same key that encrypted them. A database backup without its key holds ciphertext.

Your backup interval is therefore your recovery point objective for the durable record. Daily is a reasonable default; tighten it if you rely on Lumaft as the record for stacks whose backends prune aggressively.

What to back up

Item Where Notes
lumaft.db /data (SQLite) The database. The .owner, -wal, and -shm sidecars are not part of a backup
The PostgreSQL database Your server Use your server's own tooling
LUMAFT_TRANSCRIPT_KEY_FILE, LUMAFT_STATE_KEY_FILE Your secret manager Separately from the database. Restore the same key or the bodies stay unreadable
The backends file and deployment configuration Your configuration repository Needed to rebuild the container, not to restore data
The connected-license directory or offline license file With the database The installation ID lives in the database; keep them together

Protect backups exactly as you protect the live volume. A backup contains every account's credential hash, every token digest, every observed output, and every retained operation.

SQLite backup

Backups are cold: stop the replica, copy the file, start the replica. A copy taken while the process is running is not a valid backup unless SQLite's own online backup mechanism produced it.

docker stop -t 30 lumaft
cp /srv/lumaft/data/lumaft.db /srv/lumaft/backups/lumaft-$(date -u +%Y%m%dT%H%M%SZ).db
docker start lumaft

From a named Docker volume, copy out with the image's own runtime:

docker run --rm --entrypoint node \
  --mount type=volume,source=lumaft-data,target=/data \
  --mount type=bind,source="$PWD/backups",target=/backup \
  "$LUMAFT_IMAGE" \
  -e "require('fs').copyFileSync('/data/lumaft.db', '/backup/lumaft-backup.db');"

Verify every copy

Two checks, every time:

  1. The byte size matches the source.
  2. PRAGMA integrity_check reports ok:
docker run --rm --entrypoint node \
  --mount type=bind,source="$PWD/backups/lumaft-backup.db",target=/backup.db,readonly \
  "$LUMAFT_IMAGE" \
  -e "console.log(require('better-sqlite3')('/backup.db', { readonly: true, fileMustExist: true }).pragma('integrity_check'))"

Docker Compose and systemd packages both checks into a nightly timer.

Copy it off the machine

A backup on the same disk as the database protects against nothing that takes the disk with it. Copy to a versioned object-store bucket with its own access policy, or into your existing backup system, and give it a retention period you have written down — it bounds any deletion promise you make about the data inside.

What about snapshots?

An EBS snapshot, Azure disk snapshot, or VM snapshot of a running Lumaft is crash-consistent. It will usually recover, and it is a worthwhile second layer, but it is not a verified backup and no restore drill should depend on it alone. Snapshot the volume while the container is stopped if you want a snapshot you can trust.

PostgreSQL backup

You own the server; use its tooling. A physical base backup with WAL archiving gives point-in- time recovery; pg_dump of the Lumaft database gives a logical copy. Take logical backups while Lumaft is stopped or accept that in-flight writes may straddle the dump. The SQLite backup you took before adoption is not a PostgreSQL backup — keep it, but do not confuse the two.

Connected-licensing state lives in the shared database and is part of the backup; protect the backup as a credential. Never copy a database into a second live installation and run both.

Restore

SQLite

  1. Stop and remove the running replica.
  2. Replace /data/lumaft.db with the backup copy.
  3. Delete any leftover lumaft.db-wal, lumaft.db-shm, and lumaft.db.owner files.
  4. Restore the transcript and state key files to their original mounts if the deployment used them.
  5. Start the replica and confirm /api/v1/readiness returns 200.
  6. Sign in, open Operations, and confirm Evidence coverage shows the expected earliest retained evidence.

PostgreSQL

Restore with your server's tooling, then restart every Lumaft replica. Replicas cache nothing that survives a restart, but they must re-read the schema lineage and re-elect job ownership against the restored state.

Restoring an older release's backup

A backup is tied to the schema revision it was taken at. Start the image that took it, or a newer one — never an older one. If you need to move to a newer release after a restore, follow the normal upgrade procedure from the restored file.

Rebuilding the whole installation

When the host is gone:

  1. Provision the new host per the environment guide, with an empty data volume.
  2. Copy the latest verified backup to /data/lumaft.db, owned by UID 1000, mode 0600.
  3. Restore the key files and the backends file.
  4. Start the same image digest the backup was taken under.
  5. Confirm readiness, sign in, and check Backends and Evidence coverage.
  6. Update DNS or the load balancer target to the new host.

Do not start an empty database "to check that it works" and then swap the backup in. An empty database bootstraps a new administrator from the password file and assigns a new installation identity; connected licensing binds to that identity. Restore first, start once.

Drill it

A backup you have never restored is a hypothesis. Once a quarter, restore the latest backup onto a scratch host, start Lumaft against it, sign in, and confirm the coverage panel shows what you expect. Write down how long it took; that number is your recovery time objective, and it is the only honest one you will get.

Preserving evidence after an incident

If you need to preserve the record itself:

  1. Stop the replica cleanly rather than killing it, so the WAL checkpoints.
  2. Take and verify a cold copy of lumaft.db before doing anything else.
  3. Do not edit or delete operation-history rows by hand. Lumaft's integrity controls detect divergence, and hand-editing destroys the evidence and the ability to reason about it.
  4. Use Data & Retention → Deletion history to establish what was removed, by which action, and when.
  5. For an integrity failure reported by the expiry worker, preserve the database and its sidecars for investigation before restoring.