A backup you have never restored is a hope, not a backup. For a database-backed feature request board, that means three things need protecting: the Postgres dump, the uploaded files, and the environment file holding your secrets, the one people forget because it never shows up in a database export. Test a restore before you need one, not after.
This page is one part of a larger guide. For the whole subject in one place, see self-hosting a feature request board.
What actually needs backing up?
Most self-hosted boards store their data in three separate places, and a backup that only covers one of them is a partial backup that will surprise you at the worst moment.
| What | Where it lives | Why it gets missed |
|---|---|---|
| The database | Postgres, holding posts, votes, comments, users | Usually the only thing anyone remembers |
| Uploaded files | Screenshots and attachments on disk or in object storage | Referenced by the database but not stored in it |
| Environment and secrets | A .env file or equivalent, holding API keys and credentials | Never touched by a database dump, so it is invisible to anyone only backing up Postgres |
A restore that brings back every post and comment but leaves every screenshot as a broken link, or brings the data back onto a server that cannot send email because the API key never got backed up, fails in front of a customer rather than in a test.
How do you actually take a Postgres backup?
The standard tool is pg_dump, and the format that is worth using is the custom compressed format rather than a plain SQL file, because it supports selective and parallel restore later.
pg_dump -Fc -h localhost -U docket_user -d docket \
-f docket-2026-08-22.dump
-Fc selects the custom format, -h and -U point at the host and role, -d is the database name, and -f is where the dump goes. Run this on a schedule, not by hand: a nightly cron entry naming the file with the date stops one night's dump from overwriting the last.
Uploaded files need a separate copy, since pg_dump never sees them. A plain tar of the uploads directory, or a sync to object storage if that is where they already live, covers it. Copy the environment file alongside the dump too, not as an afterthought once a restore has already failed for a reason that has nothing to do with Postgres.
Why is a backup on the same VPS not a backup?
Because it shares every failure mode of the thing it is protecting. A disk failure takes both copies at once. So does a deleted droplet, a lapsed billing card, or a rm -rf run in the wrong directory by a tired person at midnight. None of these are exotic; they are the ordinary ways a single machine goes away, and a backup stored on that same machine goes away with it.
The fix is to move a copy somewhere a different failure would not reach: another host, object storage from a different provider, or at minimum a different disk not mounted on the same server. A one-line rsync after the dump completes closes this gap for a small setup.
rsync -az docket-2026-08-22.dump backup-host:/backups/
That single command is the difference between a backup and a copy that happens to sit next to the thing it backs up.
How do you test a restore, and how often?
A dump file that has never been restored is unverified. pg_dump can complete successfully and still produce a file that will not restore cleanly, because a corrupted disk write or a schema mismatch between versions can pass silently until someone actually tries to bring the data back.
Testing means restoring into a scratch database, not the live one, and checking the result against something already known. The two ways this goes wrong on the day it matters both have their own page: a version mismatch between the machine that took the dump and the one restoring it produces an unsupported version in the file header, and a major version bump on the image tag stops the container starting at all.
createdb docket_restore_test
pg_restore -h localhost -U docket_user -d docket_restore_test \
--clean --if-exists docket-2026-08-22.dump
psql -h localhost -U docket_user -d docket_restore_test \
-c "select count(*) from posts;"
dropdb docket_restore_test
--clean drops existing objects before recreating them and --if-exists stops that step erroring on a database that started empty, which a fresh scratch database always is. The select count(*) at the end is the actual test: compare it against what you expect from the live board, not against nothing. It is the same principle covered in reading your export file: a file you can produce but cannot open again is worth nothing.
One payment, no subscription, unlimited products.
Do this on a schedule, not once. A monthly restore test catches a broken dump within weeks rather than at the one moment it matters. The failure mode this guards against is not "we never had a backup", it is "we had a backup for eight months and never once opened it".
What if the board is just files in a git repository?
If the board stores its content as markdown files in a repository rather than rows in Postgres, most of this problem disappears. There is no database to dump, no uploads table pointing at files that might not be there, and no environment file protecting anything more sensitive than a deploy key. Every commit is already a versioned snapshot, and every clone anyone has ever made is, incidentally, a complete copy of that history. This is the model Docket's free edition uses: no database, no upload store, just files a coding agent commits.
The backup story is genuinely shorter, but it has its own failure mode, and it is easy to miss precisely because git feels safe by default: a repository is only a backup if it is pushed somewhere else. A repository sitting only on the server that serves the board, with commits made locally and never pushed, is not backed up, for the same reason a database dump on the same disk as the database is not backed up. One lost server, and the working copy and the "backup" disappear together.
The fix mirrors the database case. Push to a real remote on infrastructure a different company runs, and periodically prove you can actually get the content back from it, not just that the push succeeded.
git clone git@github.com:you/board.git board-restore-test
cd board-restore-test && git log --oneline | wc -l
A fresh clone that produces the commit count you expect is a restore test that actually tests something, in the same spirit as restoring the Postgres dump into a scratch database. It costs one command and a few seconds, and it is the only way to know the remote genuinely holds what you think it holds. Anyone migrating off Canny.io or another hosted board into a git-backed one should run this check the day the migration finishes, not months later.
Frequently asked questions
Do I need to back up anything beyond the database?
Yes, if the board stores uploaded files or holds any secrets outside the database, which most self-hosted setups do. A database dump alone will bring back posts and comments but not the screenshots they reference or the credentials the board needs to send notifications.
How often should I test a restore?
Monthly is a reasonable default for a small team, and it should also happen after any change to the backup script itself, since that is exactly when a dump silently starts producing an unusable file. The point is not the exact interval, it is that "never" is the interval most setups actually run on until something forces the question.
Is a backup on the same server better than no backup at all?
It protects against some failures, like accidentally deleting the wrong table, but not against the ones that matter most: losing the server itself. Treat it as a convenience copy for quick recovery, not as the backup, and keep at least one copy on separate infrastructure.
Does a git-based board need the same backup routine as a database-backed one?
No, it needs a shorter one. There is no database or uploads directory to protect, but the repository still has to exist somewhere other than the server serving it, and that push has to actually be verified rather than assumed.
What is the single most common backup mistake?
Backing up the database and forgetting the uploaded files or the environment file. A restore that comes back with every post intact but no working attachments, or no way to send an email, looks like a successful backup right up until someone needs it.