Backup & Disaster Recovery¶
JIM stores two pieces of state that must be backed up together as a matched pair: the PostgreSQL database and the encryption key set. Backing up one without the other leaves you unable to recover.
The database alone is not a recoverable backup
Sensitive values in the database (Connected System credentials, the SSO secret, Schedule SQL-step connection strings) are encrypted at rest. They can only be decrypted with the encryption keys that were in use when they were written. Restore a database backup onto a host that does not have the matching keys and every stored secret becomes permanently undecryptable. The rest of the data restores fine, but JIM cannot connect to any Connected System until an administrator re-enters every affected secret by hand.
🔐 What to back up¶
| Item | Where it lives | Backup source |
|---|---|---|
| Database | PostgreSQL (the bundled container, jim.database on Docker or jim-database-postgres on Podman, or your external server) |
pg_dump / volume snapshot, or your existing DBA tooling |
| Encryption keys | The jim-keys-volume volume, mounted at /data/keys; or the path in JIM_ENCRYPTION_KEY_PATH |
Copy the whole key directory |
These two are a pair. Every backup schedule that captures the database must capture the key set at the same cadence, and a restore must bring back both.
Why the keys matter¶
JIM encrypts credentials with ASP.NET Core Data Protection (AES-256-GCM). The jim.web, jim.worker, and jim.scheduler services all share the same key set so they can decrypt each other's data. Key facts that shape your backup strategy:
- Keys are not stored in the database. They are files on the key volume, deliberately kept separate from the ciphertext they protect.
- Keys are not themselves encrypted at rest. This is the correct design for a self-contained, air-gappable product with no external key-management dependency, but it makes the key volume as sensitive as the database. Protect and store its backups accordingly (see Securing key backups).
- The whole key set must be backed up, not just the active key. Key rotation generates a new active key but retains the previous ones for decryption. A backup that captured only the current key would fail to decrypt older values. Always back up the entire key directory.
🗄️ Taking a backup¶
The commands below assume the bundled PostgreSQL container and the default volume names. Confirm your actual volume name with docker volume ls | grep keys (Compose may prefix it with the project name). The Podman commands are for the default, rootful installation; for a rootless one, see Rootless commands.
1. Back up the database¶
Bundled PostgreSQL:
External PostgreSQL: use your existing database backup tooling against the JIM database.
2. Back up the encryption keys¶
From a container of JIM's own image, which is already on the server, so the command needs no internet connection, an air-gapped host's included:
From a container of JIM's own image, which is already on the server, writing the archive to its output. Podman's container log is off for it (--log-driver none), or Podman would keep a copy of the keys there:
sudo podman run --rm --network none --log-driver none --user 0 --entrypoint tar -v jim-keys-volume:/keys:ro \
"$(awk '$1 == "image:" { print $2; exit }' /opt/jim/jim.yaml)" czf - -C /keys . > jim-keys-2026-07-09.tar.gz
Not podman volume export: on a rootless installation, Podman 4's writes an empty archive while appearing to succeed.
Then check the archive holds the keys. It lists one key- file per key; if it lists none, the backup failed, whatever the command reported, so check the volume's name and take it again before you upgrade or rely on it:
If you set JIM_ENCRYPTION_KEY_PATH to a bind-mounted host directory instead of using the managed volume, simply back up that directory.
3. Store both artefacts together¶
Keep the database dump and the key archive from the same point in time as a single labelled backup set, so a restore never mixes a database with a mismatched key set.
♻️ Restoring¶
Restore both artefacts from the same backup set, then start the services.
-
Restore the encryption keys first (or at least before starting
jim.web/jim.worker/jim.scheduler), so the services find their keys on first boot:Restore onto an installation that has started once, so that its containers and volumes exist, with JIM stopped (
docker compose stop jim.web jim.worker jim.schedulerin/opt/jim). The command empties the key volume and unpacks the backup into it, from a container of JIM's own image, which is already on the server:Restore onto an installation that has started once, so that its volumes exist, with JIM stopped (
sudo systemctl stop jim.service). The first command empties the key volume, from a container of JIM's own image, which is already on the server: -
Restore the database from the matching dump, into an empty database. With JIM still stopped, the commands remove JIM's database, create it again, empty, and restore the dump into it (bundled example):
When the restore succeeds,
pg_restoreexits with code 0 and, for the bundled database, prints nothing. If it reports an error, the database is not the one the dump holds: do not start JIM.--single-transactionrestores all of the dump or none of it, so a failed restore leaves an empty database rather than part of one; resolve the error it names and run the commands again.Run them only with JIM stopped, as in step 1.
--forcedisconnects anything still connected to the database rather than refusing, because on Podman the database goes on holding JIM's connections for up to two minutes after JIM has stopped (see Dropping Lost Connections); a JIM left running would be disconnected, and would reconnect to the database while it is being restored.Restore into an empty database every time, rather than over the existing one with
pg_restore --clean.--cleanremoves only what the dump holds, so whatever was added since the backup stays: rolling back after an upgrade, what the newer release added stops parts of the backup restoring, and leaves a mixture of the two releases that the older JIM starts on without complaint.External PostgreSQL: restore likewise, with your existing database tooling, into an empty database owned by JIM's user, as Before You Install creates it.
-
Start the stack and verify:
-
Confirm secrets decrypt. Open a Connected System that uses a password (for example an LDAP Connector) and run an import, or trigger a synchronisation run. Successful connection confirms the keys and database match. A decryption error in the logs ("Failed to decrypt credential") means the key set does not match the database; restore the correct keys before proceeding.
Test your restore
A backup you have never restored is a hypothesis, not a backup. Periodically rehearse a full restore (database plus keys) into a scratch environment and confirm a Connected System still connects.
🔒 Securing key backups¶
Because the keys are unencrypted, anyone with both the database backup and the key backup can decrypt every stored secret. Treat key backups with at least the same care as database backups:
- Store them on encrypted media or encrypt the archive at rest.
- Restrict access to the same principals who may access database backups.
- Prefer keeping the two artefacts in the same protected location so they are governed by one access policy, rather than scattering them.
If the keys are lost¶
If you have a valid database backup but no matching keys, the non-secret data is fully recoverable but the encrypted values are not. Recovery means:
- Restore the database as normal.
- Re-enter every Connected System secret (service-account passwords, and any other encrypted setting) via the administration UI.
- Re-set the SSO secret and any other encrypted Service Settings.
- Re-enter any Schedule SQL-step connection strings.
There is no way to recover the original secret values without the keys; this is by design.
✅ Checklist¶
- Database backup scheduled and tested.
- Encryption key set (
jim-keys-volume/JIM_ENCRYPTION_KEY_PATH) backed up at the same cadence as the database. On Podman, the volume is in Podman's storage, so a file-level backup of the server must include/var/lib/containers, or for a rootless installation/home/jim/.local/share/containers. - Database and key backups stored together as a labelled, matched pair.
- Key backups protected with the same access controls as database backups.
- Full restore (database plus keys) rehearsed in a scratch environment, into an empty database with
pg_restorereporting no errors, and a Connected System confirmed to reconnect.
Related¶
- Deployment Guide -- volumes, production readiness checklist, air-gapped checklist.
- Configuration Reference -- the
JIM_ENCRYPTION_KEY_PATHvariable. - Service Settings -- encrypted settings and how secret changes are recorded.