Skip to content
Browse the docs

Self-hosting

Move the server to another platform

Move a Crunu server between Docker, Cloudflare, Vercel and Google Cloud with one archive: export, import into an empty database, switch DNS and verify.

Last updated

On this page

Every platform runs the same server with the same database schema, so you can move a Crunu server between your own Docker server, Cloudflare, Vercel, Google Cloud and AWS in any direction. A move is three steps:

  1. Export the whole database of the old server into one archive with crunu-server export.
  2. Import the archive into an empty database at the new place with crunu-server import, and deploy the server there with the same .env.
  3. Switch DNS for the server address to the new place, and verify.

The server is blind, so the archive contains no plaintext values: only ciphertext, public keys, token HMACs, signed documents and metadata such as names, email addresses and the audit log. You can move it over ordinary channels, but don't leave it lying around, because the metadata still shows who is on your team and what your projects are called.

Two rules a move depends on

  • Keep the same SERVER_KEY. It isn't in the archive. Sessions, share links, invitations, enrollment codes and authenticator-app secrets work only with the key they were created with. Copy .env whole and change only the database address, DATABASE_URL or SQLITE_PATH. If you also want a new key, move first and rotate after, see Rotate the server key.
  • Keep the same address, PUBLIC_ORIGIN. Sign-in signatures, share links, passkeys and the agents on your machines are tied to it. Only the DNS record changes.

crunu-server import checks the key itself. If SERVER_KEY in the settings file doesn't match the key the tokens in the archive were hashed with, it prints a loud warning.

What you need

  • a computer with Node.js 24 and a copy of the Crunu repository, after npm install for Postgres;
  • the current server's .env;
  • access to the old and the new database;
  • for Cloudflare, wrangler with a completed wrangler login.

The day before

  1. Lower the TTL of the server's DNS record to 5 minutes, so the switch spreads quickly.
  2. Do a trial move: export, import into the new database, deploy at the new place and check it at the platform's temporary address, such as *.workers.dev, *.vercel.app or *.run.app. Don't touch DNS. The real import later replaces the trial data with import … --force.

On the day

  1. Tell the team not to change environments or invite people for 15 to 30 minutes. Machines with the agent keep working, because they hold decrypted variables for the length of their lease, up to 24 hours.

  2. Stop writes on the old server if the platform allows it. With Docker: docker compose -f deploy/docker/compose.yml stop server. Anything changed on the old server after the export won't reach the new one.

  3. Export the archive.

  4. Verify the archive and keep a copy:

    Shell
    node scripts/crunu-server.mjs verify --in backup.ndjson.gz

    It checks the checksum, the record counts per table and schema compatibility, and prints the file's SHA-256. After copying the archive to another computer, compare the checksum with shasum -a 256 backup.ndjson.gz.

  5. Import the archive and deploy the server at the new place.

  6. Point the domain's DNS at the new place, or attach the domain to the new service in its dashboard.

  7. Verify, as below.

  8. Set the DNS TTL back to its usual value.

Don't delete the old place for a week. If something goes wrong, pointing DNS back is enough.

Export

All commands run from the repository directory. An archive ending in .gz is compressed. The file gets mode 0600.

Shell
# your own server, SQLite, run on the server; no Node needed there
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 backup.ndjson.gz

# Cloudflare D1
node scripts/crunu-server.mjs export --from d1:vault-company-db --remote \
  --wrangler-config dist/server/cloudflare/wrangler.toml --out backup.ndjson.gz

# Postgres with a public address: Neon, or RDS with public access
node scripts/crunu-server.mjs export --env-file .env --from postgres:env:DATABASE_URL --out backup.ndjson.gz

The export of SQLite and Postgres takes a consistent snapshot even while the server runs. For Postgres without a public address, such as Cloud SQL through its socket, open a tunnel first, for example with cloud-sql-proxy, and export through the tunnel address.

Import and deploy

Import goes only into an empty database. If it already holds server data, the command stops and changes nothing. An empty schema that the new server created on its first start counts as empty. To wipe server data at the destination and import anyway, add --force.

Import checks the archive, creates the schema of the version the archive came from, inserts the records in batches and checks their count, then applies any newer migrations.

For Postgres and D1, prepare a settings file for the new place: a copy of .env with the new database address, such as .env.new. Don't change SERVER_KEY or PUBLIC_ORIGIN.

Shell
# Vercel with Neon
node scripts/crunu-server.mjs import --env-file .env.new --in backup.ndjson.gz --to postgres:env:DATABASE_URL
sh deploy/vercel/deploy.sh --project vault-company --prod --env-file .env.new

# Cloudflare D1: deploy first, which creates an empty database, then import
sh deploy/cloudflare/deploy.sh --name vault-company --env-file .env.new
node scripts/crunu-server.mjs import --env-file .env.new --in backup.ndjson.gz --to d1:vault-company-db --remote \
  --wrangler-config dist/server/cloudflare/wrangler.toml

For your own server, create the containers without starting them, import into the volume, then start. For Cloud Run, import through a tunnel and deploy with --env-file .env.new. A move within one platform, such as to a new server or another region, uses the same commands.

Verify

  1. Run doctor against the server address. With the operator token, the metrics check confirms that all migrations are applied and cleanup runs.
  2. Sign in to the web app with the same account. Your browser session should still be valid. If you have to sign in again, the new place runs with a different SERVER_KEY.
  3. Open a few environments. Values decrypt and signatures check out.
  4. In Audit log, choose Verify integrity. The chain should be intact.
  5. On one machine with the agent, run crunu status. The service should be connected and its lease renewed.
  6. Open one share link, if you have any active ones.

If something goes wrong

SymptomWhat it meansWhat to do
the destination database isn't emptyit already holds datamake sure it's the right database. If its data isn't needed, repeat with --force
the archive checksum doesn't matchthe file is damaged or changedcopy the archive again or export again
the archive has no final linethe export didn't finish or the copy is incompleteexport again
migrations unknown to this tool versiona newer server made the archiveupdate the repository
a warning that tokens were hashed with another keythe wrong SERVER_KEY in .env.newcopy SERVER_KEY from the old place
everyone is signed out, links don't openthe server runs with a different SERVER_KEYdeploy again with the right .env. No data was harmed
agents can't reach the serverDNS hasn't updated yet, or the address changedwait for the TTL. The address must not change

To go back, point DNS at the old place. Changes made at the new place after the switch stay only there, so move them back with a reverse move.