Backups
Kestrel stores everything that matters under one directory tree. Backing up is one command.
Take a snapshot
kestrel backup /var/backups/kestrel-$(date +%F).tar.gzOr, inside the Docker container:
docker exec kestrel /kestrel backup /data/snapshot.tar.gz
docker cp kestrel:/data/snapshot.tar.gz ./Behind the scenes, kestrel backup:
- Runs SQLite
VACUUM INTOagainst a temp file. This is a transactional snapshot — the live server keeps taking writes during the backup, and the resulting file is fully self-contained (no WAL sidecar required). - Walks the sourcemap directory tree and appends every
.mapfile undersourcemaps/. - Emits a single
.tar.gzarchive:kestrel.dbat the root andsourcemaps/<project>/<release>/<file>.mapfor each map.
Restore
A restore is a plain tar extraction followed by a path swap:
# 1. Stop the running server (so we're not racing on the data dir)
docker stop kestrel
# 2. Extract the archive over the data volume
docker run --rm -v kestrel-data:/data -v $PWD:/in alpine \
sh -c 'tar -xzf /in/snapshot.tar.gz -C /data'
# 3. Start the server back up
docker start kestrelThe DB, sourcemaps, admin password file, and session secret all live under /data, so the restored container behaves identically to the source. Project tokens are unchanged because their hashes survive the round trip.
Scheduling
Run kestrel backup from cron / systemd timer / Kubernetes CronJob — there's no API to call, just a CLI invocation. Rotate old archives with whatever you already use (tmpwatch, logrotate, S3 lifecycle rules).
What's not in the backup
- The auto-generated
.admin_passwordfile. If you lose access, restart the server withKESTREL_ADMIN_PASSWORD=...set; that takes precedence over the file. Or delete the file before next start to regenerate. - Anything in
KESTREL_DATAoutside the DB and sourcemaps tree. Custom integrations that drop files there should back them up separately.
Sanity-checking a snapshot
The archive's kestrel.db is a normal SQLite file. To poke at it without restoring:
tar -xzf snapshot.tar.gz kestrel.db
sqlite3 kestrel.db 'SELECT COUNT(*) FROM events; SELECT COUNT(*) FROM issues;'If those numbers look right, the backup is good.