Backup & recovery
Note
This page is for self-hosted instances. On
Hezo Cloud you have no shell, so hezo backup is not
available to you - your database is backed up daily with point-in-time recovery and
your files are versioned, but a self-serve backup or restore is not offered yet. See
What's different from self-hosting.
hezo backup writes a portable database-and-assets migration bundle containing the
database and every uploaded asset file. It works for both database backends (embedded and
external Postgres) and both asset backends (local files
and an S3-compatible bucket), which also makes it the way
to move those two stores between backends. It does not copy the rest of dataDir.
You need data, runtime settings, and the master key
Your secrets are encrypted with your master key, and the master key is never stored with them. To restore a working instance you need all of these:
- the backup bundle for database rows and assets;
- the complete data directory for workspaces, worktrees, and instance keys;
- the config file, any files it references such as a database CA certificate, and the service definition or startup flags that select that config; and
- the twelve-word master key that unlocks the restored data.
A backup without the master key cannot decrypt the secrets inside it. Store the master key separately and safely (see Master key & encryption).
Backing up
hezo backup # database + assets → a migration bundle; stop the server first
hezo backup --output /safe/place/hezo-backup/ # choose where the bundle goes
hezo backup --no-assets # database only → a single .backup.gz file
hezo backup --config /etc/hezo/hezo.config.cjs # back up a hosted instance, any timehezo backup writes a backup bundle (default <data-dir>/backups/hezo-<timestamp>/)
containing database.backup.gz (every row plus the exact schema version it was taken at),
an assets/ tree of every uploaded file, and a manifest.json. Use --no-assets for a
database-only single .backup.gz file, or --no-database for an assets-only bundle.
Stop the server first for the embedded database and local assets. The embedded database
is single-process: hezo backup opens a second database over the same files, which is not
a safe read-only operation - it races the running server's writes and can corrupt the data.
So the command refuses to run while the server is up. The running instance holds an
advisory lock at <data-dir>/hezo.lock; backup and restore check it and stop with guidance
rather than risk the files. A hosted database and bucket are different: there
hezo backup is an ordinary consistent read against your one Postgres server, so you can
back them up any time, server running, and they pair well with your provider's own
snapshots or versioning.
If a crash ever leaves a stale
hezo.lockbehind, the next server start overwrites it, and backup/restore ignore it automatically (they verify the recorded process is actually running). You only ever delete it by hand if the error insists a server is running when you know none is.
Point the command at your data directory
hezo backup resolves its data directory the same way the server does - an explicit
--data-dir flag first, then the --config file, then the default ~/.hezo.
If your server runs with a custom data directory, the backup command has to know about it
too. Run hezo backup with the same --config the server uses, or pass
--data-dir /your/path explicitly. Otherwise the command falls back to ~/.hezo - a
directory your instance never used - and backs up the wrong database instead of yours.
- systemd / Docker: pass the same
--configto the backup command - for example,hezo backup --config /etc/hezo/hezo.config.cjsordocker exec <container> hezo backup --config <path>. - A custom dir passed only as a startup flag (
hezo --data-dir /var/lib/hezo): pass the same--data-dir /var/lib/hezotohezo backup(there is no env var to inherit).
Pointed at a directory that holds no Hezo database, the command stops with a clear error
(No Hezo database found at …) instead of silently writing a backup of an empty one - but
it's on you to point it at the right directory; a valid-but-wrong data dir is still a valid
backup of the wrong instance. The same resolution applies to hezo restore, which writes
into the resolved data directory.
The bundle does not cover the whole host. Project workspaces, git worktrees,
and instance key state live under <data-dir> and are not in a backup, so full host
recovery requires the bundle plus a copy of the data directory (a file backup or volume
snapshot works; stopped-server copies are cleanest). Also back up the config file and
service definition, including backend credentials, startup flags, and referenced files
outside dataDir such as /etc/hezo/db-ca.crt. If you choose to recreate them instead,
record the exact values and restore those files before starting Hezo. If your assets already
live in S3-compatible object storage, hezo backup reads them
straight from the bucket into the bundle - or rely on the bucket's own
versioning/replication and take a --no-assets database backup.
Restoring
hezo restore <bundle-or-file> # into the embedded database + local assets
hezo restore <bundle> --database-url postgres://… # database into an external Postgres
hezo restore <bundle> --asset-storage-url "s3://…" # assets into an S3-compatible bucketRestore replays Hezo's own migrations up to exactly the version the backup recorded,
then loads the data - so the target must be an empty database (pass --wipe to drop
and restore over a non-empty one). Asset blobs from a bundle are written into the target
asset store and checksum-verified against the restored rows (--strict-assets fails on any
blob with no matching row); use --no-assets / --no-database to restore only one half of
a bundle. A backup taken by a newer Hezo than the running binary is refused with
instructions to upgrade first. On the next server start, normal migrations bring the
restored database forward to the binary's current schema.
Physical .tar.gz snapshots written by older Hezo versions are no longer restorable.
They only ever loaded into the embedded database, so they could never be restored onto
external Postgres or moved between storage backends. If you still hold one, restore it with
a Hezo version old enough to read it, then take a fresh hezo backup - that artifact
restores onto either backend and is the format going forward.
Watching a large restore
A big instance takes minutes to restore, so hezo restore reports what it is doing at every
step - reading and decompressing the backup, recreating the schema, then a live counter for
the two long parts:
Loading rows · 42% · 1,204,000/2,860,113 rows · task_comments · 1m 12s · ~1m 40s left
Restoring asset files · 88% · 8,412/9,530 files · 3.1 GB · 4m 06s · ~32s leftIn a terminal that single line is rewritten in place. When the output goes to a pipe or a log file (systemd, Docker), the same updates are appended as ordinary lines every few seconds instead, so a restore you started over SSH or under a service manager still shows its progress in the log.
Moving between local and hosted storage
The same two commands are the migration path for the database and assets, in either
direction. Which backends you point restore at decides where the data lands; the source
is only ever read, so nothing is removed until you do it yourself.
# Local → hosted (external Postgres + S3 bucket)
hezo backup --output move/ # server stopped
hezo restore move/ --database-url postgres://… --asset-storage-url "s3://…"
hezo --config /etc/hezo/hezo.config.cjs # start against the new backends
# Hosted → local
hezo backup --config /etc/hezo/hezo.config.cjs --output back/
hezo restore back/ --data-dir ~/.hezo
hezoMigrate just one side by setting only that target on restore (e.g. only
--database-url to move the database while leaving assets where they are), or with
--no-assets / --no-database. Your master key is unchanged by a move - the encrypted
vault travels inside the backup.
Upgrades are safe to roll back
When you upgrade the binary, Hezo runs any needed database migrations on startup, and each backend has a safety net:
- Embedded: migrations run against a copy of the database, which is swapped in only on success; the previous copy is kept aside in the data directory. A failed migration leaves your original data untouched - run the previous binary.
- External: a pre-migration
hezo backupfile is written into<data-dir>/backups/automatically before anything changes (the last 5 are kept), and each migration commits its own transaction. To roll back a bad upgrade, restore that file with the previous binary (or use your provider's point-in-time recovery).
hezo restore <backup>Starting over
If you need a clean slate (or you've lost the master key and have no way back), reset the instance:
hezo --resetThis starts fresh with an empty embedded database. Your previous data isn't deleted -
the existing pgdata is renamed aside on disk - but it stays encrypted with the old
master key, so there's no recovery path for a lost key. Treat --reset as a last resort.
(For an external database, drop and recreate it with your provider's tools instead.)