Backups and restoring
For whoever is running the desk.
Three things matter, and only three:
- The database: tickets, clients, settings, everything anybody typed.
- The storage volume: attachments, inbound message originals, the location database, knowledge uploads.
.env: your configuration. On a container install the application key is not here by default; it is generated on first start and kept in the storage volume asapp-key, which is why item 2 is not only attachments. Without that key, encrypted values in the database (mailbox passwords, platform secrets) cannot be read back, even from a perfect database restore.
Everything else is an image you can pull again, with one worth knowing about and one worth not doing:
desk-tlsholds the certificates and the certificate-authority account key. Not essential, because they are re-requested automatically, but authorities limit how often you may ask for the same name, so a copy saves an awkward afternoon. On a restore to a different machine, deliberately leave it behind: you want certificates fetched fresh for wherever the desk now lives.desk-cacheis deliberately not in this list, even though it holds queued work as well as the cache. Restoring a day-old queue would re-run a day-old queue, sending email that was already sent. What protects it is durability rather than backups: it is written with an append-only log, so an ungraceful stop loses at most a second, and a politedocker compose stoploses nothing.
A nightly backup
#!/bin/bash# /usr/local/bin/desk-backup, run from the directory holding docker-compose.ymlset -euo pipefailcd /srv/support-deskset -a; . ./.env; set +aSTAMP=$(date +%F)DEST=/var/backups/deskmkdir -p "$DEST"
docker compose exec -T db mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \ --single-transaction --quick --routines "$DB_DATABASE" | gzip > "$DEST/db-$STAMP.sql.gz"
# Not "is there a file". A dump that died half way, the connection dropped, the disk filled, a table it# could not read, still gzips to something non-empty, and `test -s` waves it through. So: the archive has# to be intact, and it has to carry the closing line mariadb-dump only writes when it reached the end.gzip -t "$DEST/db-$STAMP.sql.gz"zcat "$DEST/db-$STAMP.sql.gz" | tail -3 | grep -q "^-- Dump completed"
docker run --rm -v support-desk_desk-storage:/data -v "$DEST":/backup alpine \ tar czf "/backup/storage-$STAMP.tgz" -C /data .
cp ./.env "$DEST/env-$STAMP"
find "$DEST" -mtime +14 -delete
# Off the box, which is the only copy that survives losing the host# rclone copy "$DEST" remote:desk-backups --min-age 1mchmod 700 /usr/local/bin/desk-backupecho '15 3 * * * root /usr/local/bin/desk-backup' > /etc/cron.d/desk-backupCheck the volume’s real name with docker volume ls, Compose prefixes it with the project directory.
An off-box copy is the backup. A dump on the same disk as the database protects you from a bad migration and from nothing else.
Restoring
Onto a clean host, in this order:
# 1. The compose file and the .env you backed up, the APP_KEY must be the originalmkdir support-desk && cd support-deskcp /path/to/backup/env-2026-09-14 .envcurl -O https://registry.sixnix.net/install/docker-compose.yml
# 2. Start only the database and cachedocker compose up -d db cache
# 3. Put the data backzcat /path/to/backup/db-2026-09-14.sql.gz | docker compose exec -T db mariadb -u root -p"$DB_ROOT_PASSWORD" supportdocker volume create support-desk_desk-storagedocker run --rm -v support-desk_desk-storage:/data -v /path/to/backup:/backup alpine \ tar xzf /backup/storage-2026-09-14.tgz -C /data
# 4. Everything elsedocker compose up -ddocker compose logs -f webThen check, in this order: sign in; open a ticket with an attachment and confirm the file downloads; Settings → Health; send a test email; and if you use mailboxes, confirm each still signs in, an OAuth mailbox may need reconnecting if the restore is onto a different hostname.
On a server install
Same three things, different commands:
mariadb-dump --single-transaction --quick --routines support | gzip > db-$(date +%F).sql.gztar czf files-$(date +%F).tar.gz -C /home/www/support/app storage/app .envRepository mirrors can be large. If you use the issue tracker’s repository feature, exclude
storage/app/private/repositories and let them be re-cloned, they are copies of something that exists
elsewhere.
Testing the restore
A backup nobody has restored is a hypothesis. Once a quarter, restore last night’s copies onto a scratch host and open a ticket on it. Twenty minutes, and it is the only way to discover that something you never thought about (the application key, a volume name, a file permission) was not in the backup.