Browse the docs
Self-hosting
Backups and restore
What to back up for a self-hosted Crunu server, how often, where to keep the database archive and the .env file, and how to restore without surprises.
Last updated
On this page
A self-hosted Crunu server needs two things backed up: the database and the .env settings file. They have very different secrecy, so they are stored differently.
What to back up
| What | What's inside | How secret | Where to keep it |
|---|---|---|---|
The database, as a crunu-server export archive | environment ciphertext, public keys, token HMACs, signed documents, and metadata: names of organizations, projects, environments and machines, email addresses, the audit log | no plaintext values, but the metadata is visible | anywhere reliable: cloud storage, a second disk, another provider |
The .env file | SERVER_KEY, BOOTSTRAP_TOKEN, the database address with its password, email keys | secret | a password manager, or a sealed envelope in a safe |
The database can live anywhere because nothing in it opens a variable. Environment keys belong only to people and machines, and passwords and Secret Keys never reach the server. Someone who steals the archive sees only metadata.
.env is kept separately and under lock for two reasons:
- Without
SERVER_KEY, a restored server recognizes no session, share link, invitation or authenticator-app secret, and can't decrypt alert webhooks. Environment data stays intact, but everyone signs in again, and links, invitations and alert channels have to be issued again. - Together with a database copy,
SERVER_KEYreveals the authenticator-app secrets and the alert webhook addresses and signing secrets. Never store database archives and.envin the same place.
You don't need to back up people's devices or machines. People keep their own Emergency Kits with their Secret Keys. Machines get new keys when they enroll, and a lost machine is easier to enroll again.
How often
- Every day: an automatic export, kept for 30 days.
- Before every server update, move and key rotation: an extra manual export.
- Every week: a copy of the latest archive in another place, such as another provider or an offline disk.
.env: when you create the server and after every key change.
An archive is a snapshot. The less often you take one, the more changes a restore loses.
Exporting
node scripts/crunu-server.mjs export --env-file .env --from postgres:env:DATABASE_URL --out backups/crunu-$(date +%F).ndjson.gzThe export takes a consistent snapshot and doesn't interrupt the server, except on D1, which doesn't answer while Cloudflare exports it, usually for seconds. The archive is written to a temporary file and appears under its name only when complete, so an interrupted export never looks like a finished backup.
A daily schedule on your own server
Add two lines with crontab -e on the server, adjusting the repository path:
17 3 * * * cd /opt/crunu && mkdir -p backups && docker run --rm -v crunu_data:/data -v "$PWD:/repo" -w /repo node:24-alpine node scripts/crunu-server.mjs export --from sqlite:/data/crunu.db --out backups/crunu-$(date +\%F).ndjson.gz >>backups/export.log 2>&1
47 3 * * * find /opt/crunu/backups -name 'crunu-*.ndjson.gz' -mtime +30 -deleteIn crontab, % is written as \%. Then copy the backups directory elsewhere on the same schedule, for example with rclone sync to cloud storage.
Platforms with Postgres, and Cloudflare
Run the export from any machine that can reach the database: an admin's computer, a small separate server, or a scheduled CI pipeline. It needs Node.js 24 and the repository. The server has no built-in backup scheduler.
Cloud databases have their own backups too: D1 Time Travel, Neon point-in-time restore, Cloud SQL automatic backups, RDS snapshots. Turn them on as an extra layer, not a replacement. The Crunu archive is still needed because it moves between platforms and can be verified.
Checking a backup
Once a month, check that a backup is usable:
node scripts/crunu-server.mjs verify --in backups/crunu-2026-10-24.ndjson.gzThe command checks the checksum and the record counts and prints the file's SHA-256. The best check is a trial restore into a separate database, such as an empty SQLite file on your computer, and signing in to the web app on top of it.
Restoring
Restore when the database is lost or damaged: a dead disk, a deleted database or project, or someone editing tables by hand. If the server only stopped responding, look at Monitoring and doctor first, because a restore rolls back every change made after the backup.
-
Stop the server or move traffic away so nothing writes to the database during the restore.
-
Pick the latest good backup and check it with
crunu-server verify. -
Import it. Into an empty or new database:
Shell node scripts/crunu-server.mjs import --env-file .env --in backups/crunu-2026-10-24.ndjson.gz --to postgres:env:DATABASE_URLOver the current database, add
--force. This erases all server data there first. -
Start the server with the same
.env, so with the sameSERVER_KEY. -
Check it:
doctor, sign-in, a few environments, and Verify integrity in the audit log.
What people and machines see afterwards
Everything changed after the backup is gone: new environment versions, invited people, enrolled machines, granted access. Repeat those steps.
There's a less obvious effect. Clients and agents remember the newest state numbers of each environment and reject lower ones, which protects against a server that serves old data. A restore looks exactly like that to them. Devices that saw a newer state will report a rollback or a fork and won't accept those environments:
-
A machine with the agent stops serving them, logs an alert, and
crunu runexits with code 6. Reset its memory of the server:Shell sudo systemctl stop crunu-agent sudo rm /var/lib/crunu/state.json sudo systemctl start crunu-agent -
crunuin a person's terminal: the same with~/.config/crunu/state.json. -
A browser reports a mismatch when opening an environment. Clear the site data for the server in the browser settings and sign in again with the password, the Secret Key from the Emergency Kit and the second factor.