Skip to content
VaultTerm
Browse docs

self-hosting

Backups and recovery

Back up the database, .env, and data volume; protect the one irreplaceable master key; and optionally bind it to the license or escrow it with Shamir-split recovery.

Updated Aug 18, 2026

A self-hosted VaultTerm has three things worth backing up and one thing you cannot regenerate. This page covers the routine backup, the master key, and the optional cryptographic protections that make a cloned, forged, or re-hosted instance unable to decrypt.

What to back up

WhatHow
Databasepg_dump piped through gzip (below)
.envcopy it out-of-band — it holds DEV_MASTER_KEY
db-data volumethe Postgres data volume backing the db container

Back up the database with a logical dump:

docker compose exec -T db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > vaultterm-$(date +%F).sql.gz

Restore it into a fresh stack:

gunzip -c vaultterm-YYYY-MM-DD.sql.gz | docker compose exec -T db psql -U "$POSTGRES_USER" "$POSTGRES_DB"

Migrations are forward-only, so always back up before an upgrade — the backup is also your rollback path. See Upgrading.

Restore into an empty database, never over a live one

The restore command above says “into a fresh stack” and that is a requirement, not a suggestion. The audit tables are append-only by database trigger, and one of those triggers rewrites each row’s chain link from the head of the table it is being inserted into. Loading a dump into a database that already holds audit rows therefore relinks the restored rows onto whatever is there, and does it silently.

  • Restore full, into an empty database. If you restore in two passes, restore the structure first and then the data, still into an empty database.
  • Do not use pg_dump --data-only, or pg_restore --data-only / --disable-triggers, against a database that already holds audit rows. --disable-triggers turns off the append-only guards for the duration of the load, which is the one window in which the append-only property does not hold.
  • When you need to inspect a backup, restore it to a new database and compare. Do not load it over a running one.
  • Verify the audit chain after every restore. Run npm run verify-audit-chain in server/; a restore that rolled the audit store back reports row_count_mismatch or range_hash_mismatch. A break is not automatically an attack — a restore is the most likely cause — but it must be recorded and explained rather than dismissed.

A restored older copy of the audit trail is internally consistent, so the procedure above is what prevents the mistake. The verifier is the backstop, and it is not a complete one: run it, record the result, and keep the number of sealed rows from before the restore so you can see if it moved.

The master key is irreplaceable

With the default local key provider, DEV_MASTER_KEY wraps every stored credential. It is the one thing you cannot regenerate: lose it and vault contents are unrecoverable. It lives in .env, which is why .env is on the backup list — guard that backup as carefully as the live secret. See Cryptography for the envelope-encryption model.

Optional: bind the master key to the license

You can make the license cryptographically load-bearing for decryption. With LICENSE_BIND_MASTER=1 and a BOUND_MASTER_CIPHERTEXT, the master key is sealed under an unlock derived from the root seed, the license signature, and the host fingerprint — so a copied, forged, or re-hosted instance boots but cannot open the vault. There is no removable license check to bypass; the wrong host or license simply yields the wrong unlock and every data key stays wrapped.

Provision it on the target host, after the .vtlic is in place:

cd server && npm run bind-master

This mints the master and prints the ciphertext to set as BOUND_MASTER_CIPHERTEXT; then unset DEV_MASTER_KEY. Under production, a failed unlock at boot is fatal, so an instance that cannot decrypt never serves traffic.

Optional: recovery escrow

Binding the master to its host means a dead host, a lost license, or a rebuild would otherwise orphan every data key. The recovery escrow seals the master a second, host-independent way so the legitimate owner can recover but a thief cannot. It seals the master under a random recovery key and 2-of-3 Shamir-splits that key into three shares.

cd server && npm run provision-recovery -- --mode customer-only   # or --mode managed

The mode decides who holds the shares:

ModeWho holds what
customer-only (default)all three shares go to the customer; the vendor holds nothing and mathematically cannot read or recover the vault. Losing the shares is terminal.
managedthe vendor holds one break-glass share — never enough alone — so a customer who loses a share is still recoverable from any two of three.

Recover onto new hardware with two of the three shares:

cd server && npm run recover-master

This reconstructs the master from two shares and re-seals it under the new host’s unlock, emitting a fresh BOUND_MASTER_CIPHERTEXT. Re-issue the license with a higher counter so the old host binding cannot be replayed.

Next