Backups
What to copy, why copying gryt.db on its own can hand you an empty database, and how to check a backup before you need it.
Everything a Gryt server is lives in one directory. Backing it up is copying that directory. There's one trap in the way, and it's the reason this page exists.
Copying gryt.db while the server runs is not a backup
The database is in WAL mode, so recent
writes live in gryt.db-wal until SQLite folds them back in. Copy gryt.db
on its own and you get whatever had been folded in last, which on a young
server is nothing at all. The file you end up with is an empty database,
and nothing says so until the day you restore it.
What to back up
DATA_DIR — /data inside the container, the gryt-prod-server-data volume
under Compose, and ./data if you're running the server directly.
<DATA_DIR>/
├── gryt.db messages, members, roles, invites, settings
├── gryt.db-wal writes that have not been folded in yet
├── gryt.db-shm shared-memory index for the WAL
├── server-identity-key.json the key clients pinned you by
└── <bucket>/ uploads, if you're on filesystem storageserver-identity-key.json is who your server is. Clients pin it the first
time they connect and check it every time after. A server that comes back with a
new key is a different server to everybody who had joined: no roles, no history,
and a warning on their screen. It's a few hundred bytes, and it's the one file here
you can't regenerate.
The -wal file is part of the database. It isn't a temporary file you can
skip.
If your uploads go to MinIO or another S3-compatible store rather than to the
filesystem, that's a separate volume, gryt-prod-minio-data under Compose, and
it needs its own copy.
Stop, copy, start
The simplest correct backup, and the one to use unless downtime is a problem.
docker compose stop server image-worker
docker run --rm \
-v gryt-prod-server-data:/data:ro \
-v "$PWD:/out" \
alpine tar czf /out/gryt-backup-$(date +%F).tar.gz -C /data .
docker compose start server image-workerBoth services stop because they share the volume and both write to the database. Stopping only the server leaves the image worker holding it open.
Backing up without stopping
SQLite can copy a live database properly, and the server image already has Node, which is all you need to ask it to. Nothing extra to install.
docker compose exec server node -e '
const { DatabaseSync, backup } = require("node:sqlite");
const src = new DatabaseSync("/data/gryt.db", { readOnly: true });
backup(src, "/data/gryt-backup.db").then(() => console.log("ok"));
'That produces one consistent file with the WAL already folded in, while people are still talking. Copy it out, then delete it so it isn't sitting inside the thing it's a backup of:
docker compose cp server:/data/gryt-backup.db ./gryt-backup-$(date +%F).db
docker compose exec server rm /data/gryt-backup.dbThe identity key and the uploads still have to be copied separately. This covers the database and nothing else.
Check it before you need it
Run these against the file you just made. Both take seconds:
sqlite3 gryt-backup-2026-08-28.db "PRAGMA integrity_check;"
sqlite3 gryt-backup-2026-08-28.db "SELECT count(*) FROM messages;"integrity_check answers ok. A row count of zero on a server that has
history means you hit the WAL trap above.
Restoring
Stop everything, put the directory back, start it again.
docker compose down
docker run --rm \
-v gryt-prod-server-data:/data \
-v "$PWD:/in" \
alpine sh -c "rm -rf /data/* && tar xzf /in/gryt-backup-2026-08-28.tar.gz -C /data"
docker compose up -dIf you're restoring only the database from a hot backup, put it back as
gryt.db and delete any gryt.db-wal and gryt.db-shm sitting beside it. They
belong to the database you're replacing. Leave them there and SQLite gets a WAL
that doesn't match the file it's attached to.
Hosting from the app
The desktop app keeps each server under its own directory and the app is the only thing writing to it, so stopping the app is the whole procedure. Hosting from the app has the paths.
What isn't in it
Anyone's identity except the server's own. A person's keypair lives on their own device, so restoring your server doesn't restore anybody's account. There's nothing of theirs on your disk to lose. See accounts.