Backup and Recovery
Overview
A node backup protects the encrypted data on your server if its disk fails or an upgrade goes wrong. It does not replace Safe recovery material or your admin passkey. To recover the server you also need:
- the exact node configuration;
- a working recovery admin key and its PIN;
- control of the original hostname.
The node provides a command-line backup, run on the Linux server in Bash. Allow a maintenance window: Safe users cannot use the node while it is stopped for backup.
The examples below use the standard Docker installation and its default container (unolock-node) and volume (unolock-node-data) names. If you changed those names, substitute your own. Podman users need equivalent podman commands.
The backup format is version 4. The backup:create command creates this format; you do not have to select it.
Preserve the address identity
Receive Addresses are fixed to the original node's onion service for security. Recovery on replacement hardware must keep that identity and the matching encrypted node database, including authorized publications and verified route and key pins.
- The node-managed Tor identity is stored in the encrypted node database after Tor first starts successfully, so it travels with the data backup.
- If Tor has never completed its first start, or you maintain an onion-service identity outside the node, preserve that identity separately.
A new onion identity means new Receive Addresses
Republishing old addresses or clearing their caches does not move the bindings that senders hold. Restoring an older database also loses pins learned since that snapshot. See Receive Addresses and Messaging Across Nodes.
Run only one active node with a given identity. Follow the single-active-node recovery steps below so that two services never use the same identity.
Prepare a recovery admin key
Before relying on a backup:
- Open Access in the node admin console.
- Under Admin Access Keys, register a separate Security key. Name it so its recovery purpose is clear.
- Sign out and verify that the new key and its PIN can sign in.
- For an admin-unlock node, restart the node and use the recovery key for the first unlock to prove the full recovery path.
Keep the recovery security key and its PIN separate from the backup. A backup contains the public credential verifier and the node-unlock secret wrapped for that credential. It does not contain or export the authenticator's private key, its PIN, or the admin PIN. See Admin Console.
Create and verify a container backup
Choose a backup directory on a separate disk or separately managed backup target, outside the node's data volume, for example /mnt/backup-disk/unolock. Use a new backup name each time; the date-based name below does that automatically.
1. Capture the running image
Capture the image ID directly from the installed container, so the backup uses the same software the node is running:
UNOLOCK_ADMIN_IMAGE="$(sudo docker inspect unolock-node --format '{{.Image}}')"
export UNOLOCK_ADMIN_IMAGE
printf 'Backup image: %s\n' "$UNOLOCK_ADMIN_IMAGE"
Check that it prints an image ID beginning with sha256:. This selects the image already running on your server, even if latest has changed since you installed it.
Run the following steps in the same terminal. Stop if any command fails, and do not restart the node with an unverified backup.
2. Choose the backup location
read -r -p 'Absolute path to your backup directory: ' UNOLOCK_BACKUP_ROOT
export UNOLOCK_BACKUP_ROOT
export UNOLOCK_BACKUP_NAME="node-backup-$(date -u +%Y%m%d-%H%M%S)"
3. Stop the node and save its configuration
sudo install -d -m 0700 "$UNOLOCK_BACKUP_ROOT"
sudo install -d -m 0700 "$UNOLOCK_BACKUP_ROOT/$UNOLOCK_BACKUP_NAME-recovery"
sudo docker stop unolock-node
sudo docker cp \
unolock-node:/data/unolock-node/config.json \
"$UNOLOCK_BACKUP_ROOT/$UNOLOCK_BACKUP_NAME-recovery/config.json"
sudo chmod 0600 "$UNOLOCK_BACKUP_ROOT/$UNOLOCK_BACKUP_NAME-recovery/config.json"
4. Create the backup
sudo docker run --rm \
--entrypoint unolock-node \
-v unolock-node-data:/data:ro \
-v "$UNOLOCK_BACKUP_ROOT:/backup" \
"$UNOLOCK_ADMIN_IMAGE" \
backup:create \
--config /data/unolock-node/config.json \
--output "/backup/$UNOLOCK_BACKUP_NAME"
5. Verify the backup
Wait for Backup verified: followed by the backup path before restarting the node:
sudo docker run --rm \
--entrypoint unolock-node \
-v "$UNOLOCK_BACKUP_ROOT:/backup:ro" \
"$UNOLOCK_ADMIN_IMAGE" \
backup:verify --backup "/backup/$UNOLOCK_BACKUP_NAME"
6. Start the node again
sudo docker start unolock-node
Open /admin, sign in, and choose Unlock Node. Check Diagnostics and open a test Safe. Record the backup directory and verification result in your installation notes. See Restarts and Unlocking.
If creation or verification fails, leave the node stopped, keep the error, and diagnose the failed step before retrying. Do not rely on the failed bundle.
Protect the backup files
When Docker runs with sudo, the backup and the copied config may be owned by root. Restrict their owner and storage access controls before transferring them.
Source or system-service installations
Stop the service and run:
unolock-node backup:create --config /absolute/path/config.json --output /secure/path/new-node-backup
unolock-node backup:verify --backup /secure/path/new-node-backup
From a source checkout, replace unolock-node with npm run dev --.
Older backup formats and key changes
backup:verify still accepts version 1 through version 3 bundles. Version 1 did not carry the admin recovery verifier store, version 2 did not include the node-blob object store, and version 3 did not bind protected-config lifecycle metadata. Create a new version 4 backup before relying on it for recovery.
Recovery-key status is captured when the backup is created. After disabling an admin key, create and verify a replacement backup, then retire older backups that should no longer authorize that key.
Backup creation fails for an admin-unlock node when the required verifier store or an active unlock-capable key is missing.
What the bundle contains
Backup creation does not decrypt user content or node secrets. The version 4 bundle contains the encrypted database, archive files, node-blob archive objects, any wrapped admin recovery verifier store, and a manifest with hashes. The manifest assigns each file a role:
| Role | Contents |
|---|---|
database / database-sidecar |
Encrypted SQLCipher database and any offline sidecars |
admin-recovery |
Public credential and PIN verifiers, and node-secret bundles wrapped to admin access keys |
admin-protected-config |
Non-secret descriptor binding the backup to protected-config lifecycle metadata |
archive |
Encrypted archive metadata and files under the configured archive root |
archive-object |
Encrypted node-blob object data |
The bundle does not contain the full config, the TLS private key, plaintext node keys, the admin PIN, or the authenticator private key.
backup:verify checks the manifest and every recorded size and SHA-256 digest. It does not prove that the separately held authenticator or infrastructure inputs are available.
The hosted-client signing key is encrypted inside the node database, so it is included in the backup. A database backup without a usable node-unlock recovery path cannot recover that signer.
Separate recovery inputs
The data backup deliberately is not a complete identity escrow. Keep separately protected copies, or recovery procedures, for:
- the exact live
config.jsoncaptured while the node was stopped; - the admin recovery security key and its PIN;
- the TLS certificate and private key, where you manage them yourself;
- TPM or admin-unlock recovery material;
- DNS control.
These materials have different compromise and recovery consequences. Do not put all of them in one unattended archive.
Restore a standard container backup
There is no automated restore command. A restore is an operator-controlled maintenance procedure. It must not silently overwrite a live database, fork the node identity, or start two authorities from one state set.
Restore checklist
- Verify the backup manifest before copying any file.
- Fence the original node and prove it cannot start or accept traffic.
- Restore only to a new, empty volume on an isolated recovery host.
- Inspect
manifest.jsonand reconcile every file role with the saved config. - Restore the original permanent domain, because admin passkeys are scoped to it.
- Restore the exact config, TLS, and key-protection identity through their separately controlled procedures.
- Start the node and prove that the recovery admin key unlocks it.
- Run
doctor, migrations, and local health checks before public exposure. - Verify Safe access, object integrity, hosted-client rebuild and signing, and federation.
- Expose the recovered node only after proving the old node remains fenced.
Rehearse and record your host-specific restoration procedure.
Where each part goes
This mapping applies to the standard container layout under /data/unolock-node:
| Backup path or role | Restore destination |
|---|---|
database/* |
/data/unolock-node/ using the recorded file names |
recovery/admin-verifier.json |
/data/unolock-node/admin-verifier/admin.json |
archives/* |
/data/unolock-node/archives/* |
archive-objects/* |
/data/unolock-node/archive-objects/* |
| separately saved exact config | /data/unolock-node/config.json |
Keep recovery/protected-node-secrets.json with the verified backup as a recovery-validation descriptor. It is not a replacement for the exact config and must not be used as config.json.
Stage the restore into a new volume
After verifying the backup, a default-layout restore can be staged into a new, empty Docker volume. The commands refuse a volume that already exists. Set UNOLOCK_ADMIN_IMAGE to the exact image reference, including its sha256 digest, that you are recovering with:
export UNOLOCK_ADMIN_IMAGE='REPLACE_WITH_IMAGE@sha256:REPLACE_WITH_RECOVERY_64_HEX_DIGEST'
export UNOLOCK_BACKUP_ROOT='/secure/unolock-backups'
export UNOLOCK_BACKUP_NAME='node-backup-YYYYMMDD-HHMMSS'
export UNOLOCK_RECOVERY_VOLUME='unolock-node-recovery-data'
sudo docker run --rm \
--entrypoint unolock-node \
-v "$UNOLOCK_BACKUP_ROOT:/backup:ro" \
"$UNOLOCK_ADMIN_IMAGE" \
backup:verify --backup "/backup/$UNOLOCK_BACKUP_NAME"
if sudo docker volume inspect "$UNOLOCK_RECOVERY_VOLUME" >/dev/null 2>&1; then
echo "Refusing existing recovery volume: $UNOLOCK_RECOVERY_VOLUME" >&2
exit 1
fi
sudo docker volume create "$UNOLOCK_RECOVERY_VOLUME"
sudo docker run --rm \
--entrypoint /bin/sh \
-v "$UNOLOCK_RECOVERY_VOLUME:/restore" \
-v "$UNOLOCK_BACKUP_ROOT:/backup:ro" \
-e "UNOLOCK_BACKUP_NAME=$UNOLOCK_BACKUP_NAME" \
"$UNOLOCK_ADMIN_IMAGE" \
-eu -c '
bundle="/backup/$UNOLOCK_BACKUP_NAME"
recovery="/backup/$UNOLOCK_BACKUP_NAME-recovery"
test ! -e /restore/unolock-node
test -f "$bundle/manifest.json"
test -f "$recovery/config.json"
umask 077
mkdir -p /restore/unolock-node/admin-verifier
cp "$recovery/config.json" /restore/unolock-node/config.json
cp -a "$bundle/database/." /restore/unolock-node/
cp "$bundle/recovery/admin-verifier.json" /restore/unolock-node/admin-verifier/admin.json
if test -d "$bundle/archives"; then
mkdir -p /restore/unolock-node/archives
cp -a "$bundle/archives/." /restore/unolock-node/archives/
fi
if test -d "$bundle/archive-objects"; then
mkdir -p /restore/unolock-node/archive-objects
cp -a "$bundle/archive-objects/." /restore/unolock-node/archive-objects/
fi
chown -R 10001:10001 /restore/unolock-node
find /restore/unolock-node -type d -exec chmod 0700 {} \;
find /restore/unolock-node -type f -exec chmod 0600 {} \;
'
Do not use this default-layout procedure when the saved config points the database or archives outside /data/unolock-node, or when it uses externally managed storage. Build and rehearse a host-specific mapping instead.
Start the recovered node
After fencing the original node and arranging the original permanent domain, start the staged volume with a distinct container name:
sudo env UNOLOCK_IMAGE="$UNOLOCK_ADMIN_IMAGE" \
sh packaging/docker/run-node.sh \
--domain node.example.com \
--name unolock-node-recovery \
--volume "$UNOLOCK_RECOVERY_VOLUME"
Reapply the original certificate, WebAuthn, origin, and TPM options. Do not move public traffic until the restore checklist above passes.
Backups and standby replication
Active/standby replication is not a substitute for an independent backup. It can reproduce an authorized deletion or corruption, and emergency failover can lose the most recent unreplicated changes. See High Availability.
Full reference
FAQs
Does a node backup include my users' Safe recovery material?
No. A node backup protects the encrypted data on the server. Safe users keep their own recovery material, and the node administrator cannot reconstruct missing user recovery material.
Can I take a backup while the node is running?
The documented procedure stops the node first, so plan a maintenance window. Safe users cannot use the node while it is stopped.
Is the onion identity in the backup?
The node-managed Tor identity is stored in the encrypted node database after Tor first starts successfully, so it travels with the data backup. If Tor has never completed its first start, or you manage the onion identity outside the node, preserve it separately.
Does the backup contain the TLS private key or my admin PIN?
No. The bundle does not contain the full config, the TLS private key, plaintext node keys, the admin PIN, or the authenticator private key. Keep those recovery inputs separately.
Is there a one-step restore command?
No. Restore is an operator-controlled procedure. Stage it into a new, empty volume, keep the original node fenced, and verify the recovered node before exposing it.
Is my standby server a backup?
No. Replication can reproduce an authorized deletion or corruption. Keep independent, verified backups as well.