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:
| Output | Contains | Omits | Use it for |
|---|---|---|---|
| Recovery backup | Backend-native database snapshot, referenced blobs, identities, password verifiers, external identity mappings, roles, groups, document ACLs, notification state, sealed connector refresh-token ciphertext, and token metadata | Configuration, TLS material, environment values, raw passwords, Owner-token value, browser-session values, PAT bearer values, external connector sealing key, and OAuth client secret | Recovering the same installation on the same database backend |
| Portable export | Original revisions plus versioned JSON metadata, provenance, history, and causally published text | Database, identities, credentials, authorization policy, sessions, tasks, queues, notifications, configuration, and secrets | Keeping 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-portabilityfrom 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_dumpandpg_restoreclients 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
Promoted Compose topology
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:
- Verify it from the retained medium with the matching Jiandu release and database tools.
- Restore to a new filesystem destination and, for PostgreSQL, a new empty unpublished database.
- Provision the retained Owner token at the configured protected path, then start the restored installation on an isolated origin.
- Sign in with that Owner token, and confirm that old browser cookies and PATs no longer authorize.
- Compare document and account counts, roles, groups, and representative restrictions.
- Search for known terms and download several originals across old and recent records.
- Create and verify a portable export from the restored copy.
- 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.