Backup and Restore
What to back up, in what order, and the commands to restore it — including the attachment volume that is easy to miss and the ordering rule that keeps a restore consistent.
Three stores hold state, and only two of them matter. This page is the procedure for both, and the reason the order matters.
It is also the page Docker Compose points at when it says rolling back a release means restoring the dump rather than reversing the schema. Migrations are forward-only in practice, so a restore is your rollback: if you have never tested one, you do not have one.
What Holds State
| Store | Volume | Back up? |
|---|---|---|
| PostgreSQL | pgdata | Yes. Every inbox, email row, user, domain, audit entry and setting. |
| Object storage | miniodata | Yes. Attachment bodies. The database only holds their storage_key. |
| Attachment fallback | attachments | Only if it has ever been used. api and smtpd write here when MinIO is unreachable at boot. |
| Redis | redisdata | No. Cache and pub/sub only; the reconciler rebuilds it from PostgreSQL. |
attachments is a separate volume from miniodata, mounted at
/data/attachments. If MinIO was ever down when api or smtpd started, some
attachment bodies are in there and nowhere else. Check it before you assume
miniodata is the whole picture:
docker compose exec api sh -c 'ls -A /data/attachments | head'Empty output means you can ignore it. Anything else means it is a third backup target.
The Ordering Rule
A PostgreSQL dump and an object-store copy are not snapshot-coherent. Between the two, a new email can arrive, get a row, and have its body written to the object store.
Back up the object store first, then the database. That way every
storage_key in the dump refers to an object the backup already has. The
reverse order produces rows that point at objects that were never captured — a
mail that exists in the list and 404s when opened.
For a fully consistent pair, stop the two writers for the duration:
docker compose stop api smtpd# ... take both backups ...docker compose start api smtpdMail is not lost while smtpd is down: a sending server that cannot connect
retries, which is what the 4xx class of SMTP response exists for.
Backing Up
Copy the object store
mc mirrorfrom a throwaway client container, into a local directory:bash docker run --rm \ --network "$(docker compose ps --format '{{.Project}}' api | head -1)_default" \ -v "$PWD/backup/objects:/out" \ --entrypoint sh quay.io/minio/mc -c ' mc alias set src http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" && mc mirror --overwrite src/burnerbyte /out 'Simpler, if you do not mind stopping MinIO: copy the volume wholesale.
bash docker compose stop miniodocker run --rm -v burnerbyte_miniodata:/data -v "$PWD/backup:/out" \ alpine tar czf /out/miniodata.tar.gz -C /data .docker compose start minioReplace
burnerbyte_miniodatawith your own project prefix —docker volume lsshows it.Dump the database
bash docker compose exec -T postgres \ pg_dump -U burnerbyte -d burnerbyte --format=custom --no-owner \ > backup/burnerbyte-$(date +%Y%m%d-%H%M).dump--format=customrather than plain SQL: it compresses, andpg_restorecan read it selectively.--no-ownerso the restore does not insist on a role name that may not exist on the target.Substitute your own values if you changed
POSTGRES_USERorPOSTGRES_DB; the defaults are bothburnerbyte.A dump that produced no error is not yet a backup you have. Check it lists:
bash pg_restore --list backup/burnerbyte-*.dump | headKeep the secrets with it
ENCRYPTION_KEYencrypts every stored credential — SSO client secrets, outbound SMTP passwords, storage keys — at rest, in the database. A dump restored without the same key leaves those rows undecryptable, and no amount of database is going to bring them back.Back up
.envalongside the dump, or recordJWT_SECRETandENCRYPTION_KEYwherever you keep secrets. Not in the same place as the dump, if that place is less protected than the database was.
Restoring
--clean drops every object before recreating it. Run it against the wrong
stack and it is gone. There is no undo and no prompt.
Stop everything that writes
bash docker compose stop api smtpd frontendLeave
postgresandminioup — you are restoring into them.Restore the objects first
Same direction as the backup, reversed:
bash docker compose stop miniodocker run --rm -v burnerbyte_miniodata:/data -v "$PWD/backup:/in" \ alpine sh -c 'rm -rf /data/* && tar xzf /in/miniodata.tar.gz -C /data'docker compose start minioObjects before rows, for the same reason as before: a row whose object is missing is a broken email, a spare object nobody references is harmless.
Restore the database
bash docker compose exec -T postgres \ pg_restore -U burnerbyte -d burnerbyte --clean --if-exists --no-owner \ < backup/burnerbyte-20260918-1430.dump--if-existssuppresses the errors from dropping objects a fresh database does not have yet. Some notices are normal; a failedpg_restoreexits non-zero, so trust the exit code rather than the noise.Bring it back and verify
bash docker compose start api smtpd frontenddocker compose logs --tail=20 apiThen check the three things a restore actually gets wrong, in this order:
- Sign in. If this fails,
JWT_SECRETdoes not match the one the sessions were signed with — expected, if you restored onto a different stack. Sessions are the one thing a restore is allowed to lose. - Open an email with an attachment and download it. This is the only check that proves the database and the object store agree.
- Open Settings → SSO, or outbound SMTP. If the credentials are blank or
garbled,
ENCRYPTION_KEYdoes not match.
- Sign in. If this fails,
Schedule and Retention
Nothing here is automated for you. A cron entry on the host running the dump command, plus whatever moves it off the machine, is the whole of it — and a backup that has never left the host it is backing up is not a backup.
Retention is a policy question the product does not answer: disposable inboxes expire on their own, but a dump taken today preserves mail that was meant to be deleted tomorrow. If "no opaque retention" is why you self-host this, keep backups no longer than you need them, and say so where your users can read it.
Test a restore on a scratch stack before you need one. docker compose -p burnerbyte-restoretest up -d gives you a second, isolated copy to practise on.