Skip to main content

Backup and restore

Use jiandu-portability to create the supported recovery archive. A Jiandu backup is complete only when its database snapshot and every referenced original or durable artifact represent one coherent installation—and when that archive has been restored successfully somewhere else.

Recovery backup or portable export?​

These outputs solve different problems:

OutputContainsOmitsUse it for
Recovery backupBackend-native database snapshot, referenced blobs, identities, password verifiers, external identity mappings, roles, groups, document ACLs, notification state, sealed connector refresh-token ciphertext, and token metadataConfiguration, TLS material, environment values, raw passwords, Owner-token value, browser-session values, PAT bearer values, external connector sealing key, and OAuth client secretRecovering the same installation on the same database backend
Portable exportOriginal revisions plus versioned JSON metadata, provenance, history, and causally published textDatabase, identities, credentials, authorization policy, sessions, tasks, queues, notifications, configuration, and secretsKeeping a backend-neutral readable archive

A portable export is not an operational backup. It cannot recreate who may access a restricted document. The current CLI also has no import operation that rebuilds an installation from that export.

Treat every recovery backup as secret-bearing. Encryption at rest for connector refresh tokens does not make the database snapshot public or replace owner-only storage, access control, and retention.

Create and verify a portable export​

Stop every Jiandu writer, choose a new destination, and run the export with the configuration for the installation you are preserving:

jiandu-portability export /exports/jiandu-2026-08-27 \
--data-dir /srv/jiandu-data

jiandu-portability verify-export /exports/jiandu-2026-08-27

For PostgreSQL, add the same --database-backend postgres and protected --database-url-file FILE inputs used for backup. Run verify-export again after copying the directory to its retained medium. The verifier checks the versioned manifest, safe paths, lengths, checksums, and exported object inventory; it does not prove that the export can restore identities, permissions, or a running Jiandu installation.

Before running the tool​

  • Use jiandu-portability from the same Jiandu version as the installation.
  • Stop the Jiandu process and every writer. The tool owns the installation lock for the complete operation.
  • Choose a destination that does not exist. Jiandu stages privately and publishes with a no-replace rename; it will not merge with or overwrite a target.
  • Make the destination parent and data directory owner-only and provide enough space for the database and referenced blobs.
  • For PostgreSQL, install pg_dump and pg_restore clients matching the selected supported server major, 17 or 18, and use a protected URL file with certificate-verifying TLS.

With a packaged binary, use jiandu-portability. From a source checkout, replace it with:

./scripts/run.sh --bin jiandu-portability --

Create and verify a SQLite backup​

jiandu-portability backup /backups/jiandu-2026-08-27 \
--data-dir /srv/jiandu-data

jiandu-portability verify-backup /backups/jiandu-2026-08-27

SQLite backup uses a consistent database snapshot; it does not copy a live database file and hope the WAL matches. The tool reads the blob inventory from that snapshot, streams each referenced object from the configured backend, records its length and SHA-256 checksum, and publishes the finished archive atomically.

Run verify-backup again after copying the archive to another medium. Verification checks the manifest, paths, file types and permissions, sizes, checksums, SQLite integrity and foreign keys, installation identity, household boundary, and database-to-blob inventory.

Checksums detect damage. They are not signatures and do not make an archive from an untrusted source safe.

Create and verify a PostgreSQL backup​

Put the PostgreSQL 17 or 18 URL in an owner-only regular file and configure TLS verification. Then run:

jiandu-portability backup /backups/jiandu-pg-2026-08-27 \
--data-dir /srv/jiandu-data \
--database-backend postgres \
--database-url-file /run/secrets/jiandu-database-url

jiandu-portability verify-backup /backups/jiandu-pg-2026-08-27

Jiandu verifies that the PostgreSQL server and client tools use the same supported major before backup. It uses a read-only repeatable snapshot, passes connection material through a temporary owner-only passfile, clears inherited libpq overrides, and validates that the custom archive can be completely listed and decoded.

Restore SQLite into a new destination​

Never restore over a running or published installation. The filesystem destination must not exist.

jiandu-portability restore \
/backups/jiandu-2026-08-27 \
/srv/jiandu-restored

The restored layout contains jiandu.sqlite3 and a local blobs/ directory. Restore always materializes a new local blob layout, even when the source used S3 or WebDAV. Direct restore into an existing remote namespace is unsupported.

Before the first start, put the retained Owner token at the restored copy's protected token path. The recovery archive deliberately does not contain this raw credential; if the file is absent, Jiandu creates a different token and the credential you retained has not been tested.

test ! -e /srv/jiandu-restored/owner-access-token
install -m 0600 \
/protected/offline/jiandu-recovery/owner-access-token \
/srv/jiandu-restored/owner-access-token
cmp -s \
/protected/offline/jiandu-recovery/owner-access-token \
/srv/jiandu-restored/owner-access-token

These commands intentionally print no token value. Start the restored copy on an isolated origin with that explicit Owner-token path:

export JIANDU_DATABASE_BACKEND=sqlite
export JIANDU_DATA_DIR=/srv/jiandu-restored
export JIANDU_OWNER_TOKEN_PATH=/srv/jiandu-restored/owner-access-token
jiandu

Restore PostgreSQL into an empty database​

Provision a new, empty, unpublished PostgreSQL database matching the backup's recorded 17 or 18 server major. No Jiandu process or other service may point at it. Put the target URL in a new protected file, then run:

jiandu-portability restore \
/backups/jiandu-pg-2026-08-27 \
/srv/jiandu-pg-restored \
--database-backend postgres \
--database-url-file /run/secrets/jiandu-restore-target-url

The second positional argument becomes the local restored data directory, including blobs/. The database and filesystem cannot publish with one cross-system atomic rename, so the empty, unpublished database is the safety fence.

If a failure says the database was restored but remains unpublished, do not retry against it and do not point Jiandu at it. Dispose of that target through your database procedure, provision another empty database, and rerun the restore.

Provision the retained Owner token outside the restored data directory and start only on an isolated loopback origin. The recovery configuration must point to the new URL file and its verifying CA, force the restored local blob layout, and disable the production reverse-proxy boundary:

install -d -m 0700 /run/jiandu-recovery-owner
install -m 0600 \
/protected/offline/jiandu-recovery/owner-access-token \
/run/jiandu-recovery-owner/owner-access-token
cmp -s \
/protected/offline/jiandu-recovery/owner-access-token \
/run/jiandu-recovery-owner/owner-access-token

env -u JIANDU_PUBLIC_ORIGIN -u JIANDU_INGRESS_TRUSTED_PROXY_CIDRS \
JIANDU_BIND=127.0.0.1:18077 \
JIANDU_INGRESS_MODE=loopback_http \
JIANDU_DATABASE_BACKEND=postgres \
JIANDU_DATABASE_URL_FILE=/run/secrets/jiandu-restore-target-url \
JIANDU_DATABASE_TLS_CA_PATH=/run/secrets/jiandu-restore-target-ca.crt \
JIANDU_DATA_DIR=/srv/jiandu-pg-restored \
JIANDU_BLOB_BACKEND=local \
JIANDU_BLOB_ROOT=/srv/jiandu-pg-restored/blobs \
JIANDU_OWNER_TOKEN_PATH=/run/jiandu-recovery-owner/owner-access-token \
jiandu --config /etc/jiandu/recovery.json

The recovery config must not enable a production connector, identity callback, or remote blob namespace. Open only http://127.0.0.1:18077, sign in with the retained Owner token, and keep the target unpublished. These environment values contain paths, not secret values; the database URL and Owner token stay in protected files.

Credential behavior after restore​

Restore deletes persisted browser sessions and pending OIDC transactions and revokes active personal access tokens. Every browser must authenticate again and every automation client needs a new PAT.

The archive contains sealed connector refresh-token ciphertext and its nonce, but not plaintext provider credentials, the OAuth client secret, or the external connector sealing key. Restore preserves authorized connector records. To use them again, provision the exact original sealing key and required provider client secret from separate protected storage; otherwise keep connectors disabled and reauthorize them after recovery.

The archive also omits the raw Owner token, TLS material, and configuration. Provision those separately. Put the retained Owner token at the explicit protected path used by the restored process before first start; it may live in the restored data directory for the SQLite example or in a separate read-only secret mount for PostgreSQL and Compose. Keep the retained token, connector keys, and provider client secrets outside the only Jiandu backup.

Run a recovery drill​

The repository's PostgreSQL 18 + Caddy example has an executable drill that handles its internal database URL/CA, fixed private subnet, port 443, and named volumes:

test "$(id -u)" = 1000
sudo install -d -o 1000 -g "$(id -g)" -m 0700 \
/var/lib/jiandu-recovery-drill \
/var/lib/jiandu-recovery-drill/backups \
/var/lib/jiandu-recovery-drill/secret
./examples/docker-compose/export-owner-token.sh \
/var/lib/jiandu-recovery-drill/secret/owner-access-token

PROBE_SHA256="$(sha256sum ./sample.pdf | awk '{print $1}')"
./examples/docker-compose/recovery-drill.sh \
/var/lib/jiandu-recovery-drill/backups \
jiandu-2026-08-27 \
/var/lib/jiandu-recovery-drill/secret \
doc_010000000000000000000000000 \
"${PROBE_SHA256}"
unset PROBE_SHA256

Run as uid 1000. Both directories must be existing, separate, owner-only, outside the source checkout, and inaccessible to group or other users; the secret directory must be mode 0700. The export helper creates a mode-0600, single-link owner-access-token without printing it. Use a real non-sensitive probe document ID—the inert ID above is only a shape example—and retain the exact source file whose SHA-256 you supply.

The helper pins the live image IDs and mounted named-volume identities/topology, creates the archive while Jiandu is stopped, verifies it in a networkless container with a read-only root filesystem and backup mount, and brings the live project down without deleting volumes. It then restores to a uniquely named project's fresh PostgreSQL and Jiandu volumes, starts only that copy at https://jiandu-recovery.localhost, proves a pre-backup session now returns 401, signs in with the separately retained Owner token, compares the running version, and verifies the probe original's digest. Only the exact token file is mounted read-only into the restored server container; the portability containers cannot read the token or its directory. Recovery Caddy binds only 127.0.0.1:443, and the helper rejects any resolved Compose model that publishes the restored copy on another address. This list-replacement safeguard requires Docker Compose 2.24.4 or newer.

Recovery containers and networks stop before the live project restarts. The helper then proves its original image IDs, volume identities, and mount topology are unchanged and rehashes the live probe original. Before restore, the helper prints the unique project name and creates a mode-0600 JSON evidence file beside the backup; it rewrites that file with passed or failed. Successful evidence records the images, live-volume snapshot, version, manifest digest, probe digest, duration, and result. Isolated recovery volumes remain for diagnosis and contain a second copy of the archive: read their exact project label from the evidence, then remove only those volumes through a reviewed retirement procedure. Never add --volumes to the live Compose command. Plan an outage long enough to create and verify the backup, restore the isolated copy, complete the HTTP proof, and restart the live project. The hardened example's fixed subnet and host port prevent the live and recovery projects from coexisting safely.

Every topology​

For every retained backup:

  1. Verify it from the retained medium with the matching Jiandu release and database tools.
  2. Restore to a new filesystem destination and, for PostgreSQL, a new empty unpublished database.
  3. Provision the retained Owner token at the configured protected path, then start the restored installation on an isolated origin.
  4. Sign in with that Owner token, and confirm that old browser cookies and PATs no longer authorize.
  5. Compare document and account counts, roles, groups, and representative restrictions.
  6. Search for known terms and download several originals across old and recent records.
  7. Create and verify a portable export from the restored copy.
  8. Record the date, version, operator, duration, and result outside Jiandu.

Keep at least one verified backup on storage the Jiandu process cannot continuously rewrite. Preserve the last known-good backup until a newer drill succeeds.

For error text and safe recovery actions, use the error message reference. For backend boundaries, see Database and Document storage.