Appearance
Upgrading and backups
Upgrading
A new version is a new image. With Compose:
sh
docker compose -f docker-compose.app.yml pull
docker compose -f docker-compose.app.yml up -dTo upgrade on your own schedule rather than whenever latest moves, set CROSDESK_TAG in .env to a version and change it when you choose to upgrade.
The database upgrades itself. On every start, before it accepts a single request, the app applies whatever database changes the new version brings, and logs them. There is no separate step to remember, and no window in which the new code meets the old schema. Changes already applied are skipped, so an ordinary restart costs one quick check.
Take a backup before upgrading (below). Database changes are applied forward only.
Going back to an older version
An older image refuses to start against a database that a newer one has upgraded — the log says The deployed image is older than the database — deploy the newer build instead. That is deliberate: older code would misread a newer schema. To go back, restore the backup you took before the upgrade, then start the older image.
More than one copy
Several copies of the app can run against one database — behind a load balancer, or as replicas on a container platform:
- Storage must be an S3 bucket. Each copy has its own disk, so local storage would scatter files across them.
- They upgrade safely together. Database changes are applied by one copy while the others wait (up to two minutes by default —
MIGRATION_LOCK_TIMEOUT_SECONDSraises it for a long change). - Nightly jobs run once, whichever copy reaches them first.
- Each copy holds its own pool of database connections — 10 by default,
DB_POOL_MAXto change it. Keep copies × pool under the database's connection limit.
Backing up
Three things make up an installation. Back up all three, and keep the third somewhere separate:
| What | How |
|---|---|
| The database | docker compose -f docker-compose.app.yml exec postgres pg_dump -U postgres -Fc crosdesk > crosdesk-$(date +%F).dump — or your platform's managed backups. Holds everything except the files. |
| The files | Local storage: the crosdesk_storage volume (/app/storage). S3 storage: the bucket, with your provider's versioning or replication. |
CONFIG_ENCRYPTION_KEY | The value from .env. Without it, a restored database's secrets cannot be read — the licence, the mail password, storage and sign-in provider credentials. Keep it in your password manager or secrets store, not in the same place as the database dumps. |
A backup you have never restored is a hope rather than a backup: restore one onto a spare server from time to time and sign in.
Restoring
- Start a fresh installation with the same
CONFIG_ENCRYPTION_KEYand the same or a newer image version. - Restore the database dump into its empty database (
pg_restore -U postgres -d crosdesk --clean --if-exists crosdesk.dump) and the files into the storage volume or bucket. - Restart the app. It applies any newer database changes and comes up as it was.
If the encryption key was lost, the data is intact but every secret setting must be entered again — paste the licence, and re-enter mail, storage and sign-in provider credentials in the console.
Rotating the encryption key
CONFIG_ENCRYPTION_KEY cannot simply be changed in .env: everything already encrypted with the old key would become unreadable. Re-encrypt first, then switch.
Generate the new key and save it in your secrets store before anything else:
shopenssl rand -base64 32Check what would change. The app container still holds the old key, so only the new one is passed in. This is a dry run — it lists each secret it would re-encrypt and writes nothing:
shdocker compose -f docker-compose.app.yml exec -e NEW_CONFIG_ENCRYPTION_KEY='<new key>' \ api node apps/api/dist/config-rekey.jsRun it again with
--applyat the end. Every secret is re-encrypted in one transaction — all of them or none, never half under each key.Straight away, put the new key in
.envasCONFIG_ENCRYPTION_KEYand restart:docker compose -f docker-compose.app.yml up -d. Between steps 3 and 4 the running app holds the old key and cannot read its own secrets, so workspaces are refused for no licence — do the two together, at a quiet time.
Keep the old key until the console shows the licence as Valid again.
Health checks
| Address | Answers | Use it for |
|---|---|---|
/api/health | Whether the app is running | Liveness — the image's own health check uses it |
/api/health/ready | Whether the app can reach its database | Readiness — whether to send it traffic |
Both answer whatever the licence says, so a lapsed licence never looks like a crash.
Nightly jobs
The app tidies up after itself overnight, in server time (UTC in the published image):
| Time | Job |
|---|---|
| 01:00 | Marks files no longer attached to anything, and uploads never finished, as deleted; deletes the stored files of those marked more than a day ago (attachment.sweepGraceHours) |
| 01:30 | Removes expired invitations, used sign-in codes and spent password-reset links |
| 02:00 | Removes screen recordings past their retention — see Recordings settings |
| 03:00 | Compares storage with the database: deletes any stored file no record names and aborts unfinished uploads nothing owns, once they are more than a day old. Only Crosdesk's own files under tenants/ are touched, so a bucket shared with something else is safe |
Nothing needs scheduling: they run inside the app.