Backups & restore
Set up backups before going live, and test a restore at least once. A backup you have never restored is a guess, not a backup.
What to back up
Two things, and you need both:
| What | Why |
|---|---|
The data root (MEMSHIP_DATA_ROOT, e.g. /srv/openmemship/data) | The database, uploads, secret.key, session.key, TLS certificates — everything that must survive |
.env | Your generated secrets. Losing SECRET_KEY and MEMSHIP_SECRET_KEY means losing access to encrypted data even with a perfect database dump |
Because every persistent path is a bind mount under one directory, backing memship up is copying that directory:
sudo tar czf memship-$(date +%F).tar.gz \
-C /srv/openmemship data \
-C /srv/openmemship/app .envsudo is needed because data/postgres is owned by uid 70 and mode 0700 — that is Postgres protecting its own files, not a misconfiguration.
A file-level copy of
data/postgreswhile the database is running is not crash-consistent. For a dependable database backup usedb-backup.shbelow, which runspg_dumpinside the container. Use thetarcopy for uploads, certificates and config — or stop the stack first if you want the whole tree in one consistent shot.
Database backups
./scripts/db-backup.shThe dump is written to $MEMSHIP_DATA_ROOT/backups/ as a timestamped .sql.gz, mode 600 and owned by the account that runs the script — it is a complete copy of your members’ data, so it is not left world-readable. Dumps older than 10 days are removed automatically.
For unattended backups, schedule it — nightly at 03:30, for example:
30 3 * * * cd /srv/openmemship/app && ./scripts/db-backup.shThe script resolves the backup directory from
MEMSHIP_DATA_ROOTin.env, the same way Compose does, so it always writes where thedbcontainer’s/backupsmount points. If you move the data root, both the backups and the script follow it — no second setting to update.
Restore
The restore script lists the available dumps and lets you pick one. It is a dry-run by default, so you can confirm the target before anything changes:
./scripts/db-restore.sh # dry-run: shows what would happen
./scripts/db-restore.sh --confirm # actually restore, asking you to type a confirmationBoth of those need a terminal — the picker and the confirmation both read from one. For a restore run from a script, a cron job or a non-interactive ssh host '…', name the dump and pass --yes:
./scripts/db-restore.sh --confirm --yes memship_20260819_141405.sql.gzRestoring overwrites the current database. Check you are pointed at the environment you think you are.
Before it drops anything the script checks that the archive is readable and looks like a pg_dump file, and dumps the current database to memship_pre-restore_<timestamp>.sql.gz in the same directory. If the restore then fails part-way it says so, exits non-zero, and leaves the API stopped on purpose — starting it would run migrations against the half-restored database and leave you with an instance that answers “healthy” and contains nothing. Put back what you had with the pre-restore dump it names.
When several restores fail in a row, each one writes its own pre-restore dump — and the newest is the one taken from the already-damaged database. Restore the file the failure message named, not whichever is most recent.
Restoring onto a new server
- Run
vps-bootstrap.shandinstall.shas normal. - Stop the stack:
docker compose down. - Restore
.envfrom your backup — the old one, not the newly generated one. A freshMEMSHIP_SECRET_KEYcannot decrypt SSO and payment-provider credentials stored in the old database. - Unpack the data root over the new one, keeping ownership:
sudo tar xzf … -C /srv/openmemship. docker compose up -d, then./scripts/db-restore.sh --confirmif you are restoring from apg_dumprather than the directory copy.
Off-server copies
The backups/ directory sits on the same disk as the database it protects, so on its own it survives nothing worse than a bad migration. Copy the dumps somewhere else on a schedule — object storage, another host, anywhere with a different failure mode — or a single server loss takes your only copy with it.