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
| Part | Holds |
|---|---|
| Postgres | Everything 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 bucket | Attachments and brand logos (S3_BUCKET). |
ENCRYPTION_KEY and SESSION_SECRET | Needed 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:
| File | Contents |
|---|---|
database.dump | pg_dump in custom format |
storage.tar.gz | The file storage volume |
backup.txt | The time, the image and the number of migrations |
SHA256SUMS | Checksums |
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
.envsafely 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.shreplaces the current database and files. It asks you to typerestorefirst.
cd deploy
sh restore.sh backups/20261022T030000Z
- It checks the checksums and stops the app.
- It re-creates the database from
database.dump. - It replaces the file storage with
storage.tar.gz. - 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_KEYis 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" \
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:
-
Stop the desk.
-
Restore the bucket (sync the backup copy back, or point
S3_*at the copy). -
Restore the database into an empty one:
createdb -h db.example.com -U desk deskTo replace an existing database, drop and recreate it first, or add
--clean --if-exists. -
Start the desk with the same
ENCRYPTION_KEYandSESSION_SECRETas 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 /healthzanswers200with"database":"ok"and nopendingMigrations.- The log has no
can't decrypt the stored valuelines. - 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.