Backups and restore

Verified backups from the admin panel or the command line, what an archive contains and why it is a secret, dry-run and live restores, manual copies, per-item exports, and the one thing you cannot recover without: the encryption key.

ServerDesktop appAndroid app

The database is encrypted at rest and the key is not recoverable. A without its key is noise. Every archive the server makes includes the key for that reason, which makes the archive itself a secret: keep it somewhere private.

From the admin panel

Admin, Server settings, : create an archive, verify one, download it, delete it, or restore from it. Archives land in ODM_BACKUP_DIR (default ~/odm-backups). A world hosted from the desktop or Android app has no Backup section; see Hosting from your computer for keeping a copy of a desktop world.

From the command line

node scripts/odm-backup.mjs                    # to ~/odm-backups
node scripts/odm-backup.mjs /path/to/backups

node scripts/odm-restore.mjs odm-backup-<stamp>.tar.gz /tmp/odm-restore-check   # dry run
node scripts/odm-restore.mjs odm-backup-<stamp>.tar.gz --live

Under , run each through ./scripts/docker-entrypoint.sh inside the container so the key is loaded. Stop the app before a command-line ; the live restore refuses to run while the server is listening.

What a backup does

  1. Opens the encrypted database with the key to prove it is readable, and checkpoints the write-ahead log.
  2. Stages the persistent state: data/ (the database, , the key file), uploaded and generated media, local model files, and .env.server when present.
  3. Writes a manifest and a timestamped odm-backup-<stamp>.tar.gz, and prints its SHA-256 (archives over 2 GiB are hashed as a stream).

What a restore does

A verifies before it touches anything: it rejects absolute and parent-traversal paths in the archive, extracts to a temporary folder, reads the archived key, opens the database, runs an integrity check, and reports the campaign, user and message counts it found. The dry run stops there. --live replaces the running install's state with the archive's, and refuses while the app is listening on its port.

Manual copies

If you would rather copy files: both data/local-roleplay.sqlite and its key are required, plus public/uploads/; add data/worlds/ if packs were installed. public/generated* is optional (pictures and audio can be regenerated). Under the odm-data, odm-uploads, odm-generated and odm-audio volumes are the same set.

Per-item exports

  • A character as .odm-character.json, portrait inside.
  • A as .odm-workshop.json, pictures inside.
  • A campaign's story as HTML, ODT or DOCX: a reading copy, not a save.

These import on any server. Play state (the , the seats, the live sheets) moves only with the database.

When to back up

Before every update, and after a session you would mind losing. A world hosted on a phone cannot be archived: export what you want to keep, and remember that uninstalling the app deletes the world.

Checked against desktop and Android app 0.17.2 and server 0.26.2 on 2026-10-11. Something wrong or missing? Open an issue.

Download · v0.17.2
Server app · v0.26.2

Optional. Runs the table for a whole group on one machine that stays on. Everyone else just installs the app.

Free & open source · MIT licensed · served from GitHub releases