Skip to content
Documentation

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:

  1. Open Access in the node admin console.
  2. Under Admin Access Keys, register a separate Security key. Name it so its recovery purpose is clear.
  3. Sign out and verify that the new key and its PIN can sign in.
  4. 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.json captured 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

  1. Verify the backup manifest before copying any file.
  2. Fence the original node and prove it cannot start or accept traffic.
  3. Restore only to a new, empty volume on an isolated recovery host.
  4. Inspect manifest.json and reconcile every file role with the saved config.
  5. Restore the original permanent domain, because admin passkeys are scoped to it.
  6. Restore the exact config, TLS, and key-protection identity through their separately controlled procedures.
  7. Start the node and prove that the recovery admin key unlocks it.
  8. Run doctor, migrations, and local health checks before public exposure.
  9. Verify Safe access, object integrity, hosted-client rebuild and signing, and federation.
  10. 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.

Back to Self-Hosted Overview