Skip to main content

Backups

A complete backup has three parts: the Postgres database, the object storage bucket, and the two secrets. Without the secrets, a restored database is only partly usable.

Back up database and files → Copy off the server → Restore → Check the restore

What to back up​

PartHolds
PostgresEverything except files: cases and conversations, contacts and accounts, users and sessions, forms, portals, rules, the audit log, installation settings, and the job queue (pgboss schema).
The object storage bucketAttachments and brand logos (S3_BUCKET).
ENCRYPTION_KEY and SESSION_SECRETNeeded to read what the database holds encrypted, and to keep links already sent working.

[!WARNING] Without file storage set up, files only live in the desk's memory and can't be backed up at all. Set up storage before you go live (see configuration).

With the Docker Compose setup​

The recommended setup comes with two scripts in deploy/: backup.sh and restore.sh.

backup.sh​

backup.sh writes the database and the bundled file storage to backups/<time>/, or to BACKUP_DIR from .env:

FileContents
database.dumppg_dump in custom format
storage.tar.gzThe file storage volume
backup.txtThe time, the image and the number of migrations
SHA256SUMSChecksums

File storage pauses for a few seconds while it is copied; the app keeps running.

cd deploy
sh backup.sh
  • Run it on a schedule, for example nightly from cron (0 3 * * * cd /srv/itsmengine/deploy && sh backup.sh).
  • Copy the folders off the server.
  • Keep .env safely as well, separately from the backups: the backups don't contain the secrets.

With external S3 storage, backup.sh backs up the database only; back up the bucket as described in backing up files.

restore.sh​

[!CAUTION] restore.sh replaces the current database and files. It asks you to type restore first.

cd deploy
sh restore.sh backups/20261022T030000Z
  1. It checks the checksums and stops the app.
  2. It re-creates the database from database.dump.
  3. It replaces the file storage with storage.tar.gz.
  4. It starts everything again.

Restore with the same ENCRYPTION_KEY and SESSION_SECRET in .env as when the backup was made, and the same app version or a newer one. Then check the restore.

A tested run of both scripts is part of the release checks: they back up a case and its attachment, delete everything with docker compose down -v, restore, and find the case and attachment again.

The two secrets​

ENCRYPTION_KEY​

ENCRYPTION_KEY encrypts, in the database:

  • form signing secrets (server mode) and webhook signing secrets
  • each user's two-factor (TOTP) secret
  • secret installation settings: the SMTP address, the Resend API key, the incoming email secret, the storage secret key and the Turnstile secret key
  • the copy of each case link kept so later emails can include it

[!WARNING] If ENCRYPTION_KEY is lost or changed, none of these can be read.

What then has to happen:

  • Every form secret and webhook secret has to be rotated.
  • Every user's two-factor sign-in has to be reset.
  • The secret installation settings have to be entered again.
  • New emails can't carry existing case links. Links requesters already have keep working, because they are looked up by a hash.

The desk logs can't decrypt the stored value (was ENCRYPTION_KEY changed?) for each stored setting it can't read.

SESSION_SECRET​

SESSION_SECRET signs:

  • the cookies for requesters' case pages
  • the satisfaction survey links in emails already sent
  • form start tokens
  • the setup wizard's cookie
  • the Message-IDs of outgoing emails

If it changes, survey links in old emails stop working and requesters have to open their case link again; email replies still reach their case through the reply address. Agents stay signed in (their sessions are kept in the database).

Where to keep them​

Keep both in your secret store, where whoever restores a backup can find them, but not inside the backup files themselves: a copied backup alone then reveals no secrets.

Backing up the database​

pg_dump in custom format, from any machine that reaches the database:

pg_dump --format=custom --no-owner \
--file="desk-$(date +%Y-%m-%d).dump" \
"postgresql://desk:[email protected]:5432/desk"

With the compose example from installing, run it inside the Postgres container:

docker compose exec -T postgres pg_dump --format=custom --no-owner -U desk desk > "desk-$(date +%Y-%m-%d).dump"

Run it on a schedule (cron, your platform's scheduled tasks, or the database host's own backups) and keep copies off the server. A dump taken while the desk runs is consistent; there is no need to stop it.

Backing up files​

Copy the bucket to a second location with any S3 tool. For example with the AWS CLI:

aws s3 sync s3://desk s3://desk-backup --endpoint-url https://storage.example.com

or with rclone, which can also copy between providers:

rclone sync storage:desk backup:desk-backup

Versioning on the bucket, where your provider offers it, also protects against files deleted by mistake.

[!TIP] Back up the database and the bucket at about the same time. A case that points to a file not in the bucket shows the attachment as missing; files in the bucket that no case points to do no harm.

Restoring​

Without the scripts:

  1. Stop the desk.

  2. Restore the bucket (sync the backup copy back, or point S3_* at the copy).

  3. Restore the database into an empty one:

    createdb -h db.example.com -U desk desk
    pg_restore --no-owner --dbname="postgresql://desk:[email protected]:5432/desk" desk-2026-10-21.dump

    To replace an existing database, drop and recreate it first, or add --clean --if-exists.

  4. Start the desk with the same ENCRYPTION_KEY and SESSION_SECRET as when the backup was taken. Use the same image version or a newer one; a newer image applies its migrations on start (see upgrades).

Checking a restore​

  • GET /healthz answers 200 with "database":"ok" and no pendingMigrations.
  • The log has no can't decrypt the stored value lines.
  • Settings → Installation shows the secret settings as saved, and Send test email and Test upload succeed.
  • Sign in with two-factor, open a case with an attachment, and download the attachment.
  • For a server-mode form or a webhook, send a test (Send test event on the webhook) and check that the receiving side accepts the signature.

[!TIP] Try a restore on a spare database now and then, not only when you need it.