Skip to content
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

WhatWhat's insideHow secretWhere to keep it
The database, as a crunu-server export archiveenvironment ciphertext, public keys, token HMACs, signed documents, and metadata: names of organizations, projects, environments and machines, email addresses, the audit logno plaintext values, but the metadata is visibleanywhere reliable: cloud storage, a second disk, another provider
The .env fileSERVER_KEY, BOOTSTRAP_TOKEN, the database address with its password, email keyssecreta 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_KEY reveals the authenticator-app secrets and the alert webhook addresses and signing secrets. Never store database archives and .env in 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

Shell
node scripts/crunu-server.mjs export --env-file .env --from postgres:env:DATABASE_URL --out backups/crunu-$(date +%F).ndjson.gz

The 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:

Text
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 -delete

In 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:

Shell
node scripts/crunu-server.mjs verify --in backups/crunu-2026-10-24.ndjson.gz

The 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.

  1. Stop the server or move traffic away so nothing writes to the database during the restore.

  2. Pick the latest good backup and check it with crunu-server verify.

  3. 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_URL

    Over the current database, add --force. This erases all server data there first.

  4. Start the server with the same .env, so with the same SERVER_KEY.

  5. 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 run exits 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
  • crunu in 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.