Backup and restore¶
How to take, verify, schedule and restore backups of a LogsTotal instance.
Commands run on the deployment host (the control plane, for a fleet), from the install directory.
Backups & restore¶
./logstotal backup # detect the database → dump → verify that file → write a receipt
./logstotal backup:sqlite # → backups/logstotal-YYYYMMDD-HHMMSS.db
./logstotal backup:postgres # → backups/logstotal-pg-YYYYMMDD-HHMMSS.sql.gz
./logstotal backup:uploads # → backups/logstotal-uploads-YYYYMMDD-HHMMSS.tar.gz (the stored log files)
./logstotal backup:verify # check the newest backup, or BACKUP_FILE=...
./logstotal backup:prune # delete backups older than BACKUP_RETENTION_DAYS (default 14)
./logstotal restore:sqlite # restore the newest SQLite backup, or BACKUP_FILE=...
./logstotal restore:postgres # restore the newest PostgreSQL dump, or BACKUP_FILE=...
./logstotal backup is the one to use. It works out which database the instance really uses, the same way the Docker entrypoint does: an explicit DATABASE_URL wins; otherwise PostgreSQL is used only when POSTGRES_PASSWORD is set and either postgres is in COMPOSE_PROFILES or POSTGRES_HOST names an external server. So a leftover password never makes it dump the wrong database. It then dumps, verifies the file it just wrote, and records a receipt in backups/last-verified.json (database type, file name, UTC time and app version — no credentials). A failed dump or a failed verification fails the command. ./logstotal upgrade runs it before every upgrade.
What a complete backup contains¶
- The database —
./logstotal backup: jobs, findings, entities, users. - The uploaded files —
./logstotal backup:uploadswith local storage. With S3 or Garage, back up the bucket or the Garage data volume instead. .env— it holdsSECRET_KEY(losing it signs everyone out and, unlessENRICHMENT_ENCRYPTION_KEYis set, makes the stored enrichment and AI provider tokens unreadable) and the database credentials. No release archive or backup task includes it: keep a copy in your secrets manager.
Verifying a backup¶
./logstotal backup:verify checks a file by its name:
.db(SQLite) — copies it to a temporary file, never the live database, and runsPRAGMA integrity_check. It passes when the result isokand the schema is not empty..sql.gz(PostgreSQL) — the gzip data is intact, not empty, and starts with thePostgreSQL database dumpheader..tar.gz(uploads) —tarcan list the whole archive.
It checks the newest file in backups/ unless you pass BACKUP_FILE=path/to/file, and exits non-zero on any failure. This checks the file, not that the data can be recovered — see Rehearse a restore.
Schedule backups¶
| Deployment | How often | Notes |
|---|---|---|
| Single host | Daily | A SQLite backup is safe while the app runs; run a PostgreSQL dump when load is low |
| Fleet | Daily, on the control plane | Back up PostgreSQL and the Garage data volume separately |
A crontab entry for a daily verified backup, followed by removing backups older than 14 days:
0 3 * * * cd /opt/logstotal && ./logstotal backup && ./logstotal --yes backup:prune
--yesanswers the confirmationbackup:pruneasks for. To keep backups for longer, set the variable in front of the command:BACKUP_RETENTION_DAYS=30 ./logstotal --yes backup:prune.BACKUP_RETENTION_DAYS=0deletes every file inbackups/.- Pruning deletes only files directly under
backups/, never the release snapshots inbackups/releases/. - Cron runs with a minimal
PATH../logstotalbrings its own copy of Task, butdocker— andpg_dump/psqlfor an external PostgreSQL server — must be on thePATHcron uses; setPATH=at the top of the crontab if they are elsewhere. - Send the output somewhere you will read it, and alert on a non-zero exit.
- This covers the database only. Schedule the uploads (or object store) and a copy of
.envseparately.
Select the correct target¶
DATABASE_URL selects the database. For SQLite, BACKUP_CONTEXT=docker or BACKUP_CONTEXT=host says whether the path is the Docker one or a host one; set it whenever both a development database and a Docker one exist on the machine. The Docker database /data/logstotal.db is ./data/logstotal.db on the host, including when restoring into a missing file. A custom path outside the standard mounts needs a host path and BACKUP_CONTEXT=host.
The tasks print the SQLite path they use. For PostgreSQL, an explicit external DATABASE_URL is used even if a bundled database container is running. An external server needs pg_dump and psql on the host (apt install postgresql-client).
Restore SQLite on Docker¶
Choose the backup to restore and keep a separate copy of the current database. Stop both application services so nothing writes during the restore:
docker compose stop web worker
BACKUP_CONTEXT=docker ./logstotal restore:sqlite BACKUP_FILE=backups/logstotal-YYYYMMDD-HHMMSS.db
./logstotal docker:up
./logstotal doctor:docker
The restore asks for confirmation, verifies the backup first, and refuses an empty file or one with no schema. It also refuses while the app is running; FORCE=yes overrides that.
If it reports Permission denied, the data directory is owned by the container's user. Run the restore from a shell that can write to it, from the same install directory and with BACKUP_CONTEXT=docker.
Restore PostgreSQL on Docker¶
Run on the control plane. Stop every fleet worker first (./logstotal deploy:stop stops a whole fleet), then the local application services. Keep PostgreSQL running: ./logstotal docker:down would remove the container used below.
The target must be an empty database: a plain SQL dump creates tables, it does not replace existing ones. This example uses the bundled database's default user, logstotal, and restores into a new database so the current one stays available:
docker compose stop web worker
docker compose up -d postgres
docker compose exec -T postgres createdb -U logstotal logstotal_recovered
DATABASE_URL='' POSTGRES_DB=logstotal_recovered \
./logstotal restore:postgres BACKUP_FILE=backups/logstotal-pg-YYYYMMDD-HHMMSS.sql.gz
With a different PostgreSQL user, use it in createdb and set POSTGRES_USER for the restore. For an external server, have its administrator create the empty database and pass its connection URL to the restore.
After a successful restore, set POSTGRES_DB=logstotal_recovered in the control plane's .env, and update any explicit DATABASE_URL and SYNC_DATABASE_URL on every host to the new database. Recreate the control plane, check it with ./logstotal doctor:docker, then start the workers. Keep the old database until you have checked record counts, sign-in, uploads and an analysis. If the restore fails, it rolls back as one transaction; leave the application stopped and read the error before retrying.
To recover onto an older release, put the matching code back before starting the application — see Rollback. Starting newer code against the restored database would migrate it forward again.
Rehearse a restore¶
./logstotal backup:verify proves a file is intact, not that the instance can be recovered from it. From time to time, restore onto a separate instance, compare record counts, sign in, open an uploaded file and run a sample analysis. Record which backup you tested and how long recovery took. Never point a rehearsal worker at the production Redis or object store.
Related: Upgrade and roll back · Storage and retention · Move from SQLite to PostgreSQL