Docs

Backup and restore

How to back up and restore Localization OS on both storage backends, with a rehearsable recovery path.

One-command full backup (recommended)#

The CLI's db backup --full command writes a single archive covering the database, the uploads directory, and the encryption keyfiles, with an embedded manifest. A matching verify command checks the manifest, per-file checksums, database integrity, and keyfile presence; a restore command restores the whole install from the archive. Restore is destructive: it is confirm-gated, and refused while the server is running. The manual, backend-specific recipes below remain valid, and explain what that archive actually contains.

loc-tms db backup --full archive.tar.gz
loc-tms db verify-backup archive.tar.gz
loc-tms db restore archive.tar.gz

The full archive contains live key material along with your data. Store it encrypted, and control access to it the same way you would the running install's own secrets.

The one rule that matters#

Note

Back up the data directory together with the database, as one consistent set.

The data directory holds:

  • uploaded source documents, which may be encrypted at rest,
  • the two secret keyfiles, only when they are not injected via environment,
  • rulebooks, prompt files, and other runtime artifacts,
  • the SQLite database file and its sidecars, in SQLite mode only.

If you back up the database but lose the data directory, you lose the uploads and, in keyfile mode, the encryption key, which makes every encrypted upload and stored connector token permanently unreadable. If you use environment variables or a secret manager for the keys instead, the data directory still holds the uploads, so it still must be backed up; just make sure your secret manager is backed up too.

SQLite backend (default)#

Everything lives under the data directory, so a backup is essentially "copy the data directory consistently." The only subtlety is the SQLite write-ahead log.

Hot backup (app running): checkpoint the log first

The database runs in write-ahead-log mode, so recent writes may be sitting in a separate log file, not yet in the main database file. Copying the main file alone can miss them. Two safe options:

Option A: the built-in backup command (preferred; atomic, no downtime):

# writes a single consistent snapshot (folds in the write-ahead log)
sqlite3 /data/loc_tms.db ".backup '/backup/loc_tms.db'"

Then also copy the rest of the data directory: copy the uploads directory, and copy the two secret keyfiles at the root of the data directory alongside it.

cp -a /data/uploads /backup/uploads

Option B: checkpoint, then copy the database files consistently:

sqlite3 /data/loc_tms.db "PRAGMA wal_checkpoint(TRUNCATE);"
# copy the db and its sidecars together, in the same pass
cp -a /data/loc_tms.db /data/loc_tms.db-wal /data/loc_tms.db-shm /backup/ 2>/dev/null || true

Copying the database file together with its sidecars keeps them consistent.

Cold backup (app stopped): simplest and safest

Stop the app and copy the whole data directory. With no writer, everything is consistent and there is nothing to checkpoint:

docker compose stop app
docker run --rm -v loc_data:/data -v "$PWD":/backup alpine \
  tar czf /backup/loc_data-$(date +%F).tar.gz -C /data .
docker compose start app

This tarball is a complete backup: database, uploads, and keyfiles in one file.

Restore (SQLite)

docker compose stop app        # or ensure nothing is writing
# restore the whole data directory from the tarball, including its keyfiles
docker run --rm -v loc_data:/data -v "$PWD":/backup alpine \
  sh -c "rm -rf /data/* /data/.[!.]*; \
         tar xzf /backup/loc_data-YYYY-MM-DD.tar.gz -C /data"
docker compose start app

If you took an Option-A snapshot instead, drop the restored database file into the data directory (removing any stale write-ahead sidecars) and restore the uploads directory and keyfiles alongside it. On next boot the app manages its own schema against the restored database.

Postgres backend (optional)#

On Postgres, the database lives in Postgres, and the data directory still holds uploads and keyfiles. You must back up both.

Back up the database

# bundled db service (compose postgres profile)
docker compose exec db pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB" > loc_tms-$(date +%F).dump
# external / managed Postgres
pg_dump -Fc "postgresql://user:pass@host:5432/locos_app" > loc_tms-$(date +%F).dump

The custom dump format is compact and restores selectively.

Back up the data directory (uploads and keyfiles)

docker run --rm -v loc_data:/data -v "$PWD":/backup alpine \
  tar czf /backup/loc_data-$(date +%F).tar.gz -C /data .

There is no database file here in Postgres mode, but uploads and the keyfiles are, so this step is still required.

Restore (Postgres)

# 1) restore the database (into an empty/target database)
docker compose exec -T db pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists < loc_tms-YYYY-MM-DD.dump
#    external:
pg_restore --clean --if-exists -d "postgresql://user:pass@host:5432/locos_app" loc_tms-YYYY-MM-DD.dump

# 2) restore the data directory (uploads + keyfiles)
docker run --rm -v loc_data:/data -v "$PWD":/backup alpine \
  sh -c "tar xzf /backup/loc_data-YYYY-MM-DD.tar.gz -C /data"

# 3) start the app; it applies any pending migrations for you
docker compose up -d --build

The dump was taken from a migrated schema, so it restores at the same revision, and any migration step on boot is then a no-op, or applies any newer revisions. Make sure the restored keyfile matches the key that encrypted the data, or encrypted uploads and connector tokens will not decrypt.

Checklist#

  • Backup includes the database: the SQLite file via the built-in backup command or a cold copy, or a Postgres dump.
  • Backup includes the data directory: uploads and keyfiles, captured together with the database.
  • The encryption keyfile, or your secret manager's copy of the encryption key, is recoverable. Without it, encrypted data is lost forever.
  • Restores are tested end to end: restore into a scratch instance and log in.