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
| What | How |
|---|---|
| Database | pg_dump piped through gzip (below) |
.env | copy it out-of-band — it holds DEV_MASTER_KEY |
db-data volume | the 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, orpg_restore --data-only/--disable-triggers, against a database that already holds audit rows.--disable-triggersturns 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-chaininserver/; a restore that rolled the audit store back reportsrow_count_mismatchorrange_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:
| Mode | Who 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. |
managed | the 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
- Keys and licensing config — the binding and escrow variables.
- Cryptography — envelope encryption and the key hierarchy.
- Upgrading — back up first; the backup is the rollback path.