Backups & restore
What to back up on a self-hosted Flow CMS — Postgres, media, and the secrets that must never be lost — and how to restore cleanly.
A self-hosted Flow CMS has exactly three things worth backing up. Get these three right and you can rebuild an instance from nothing.
What to back up
| What | Where it lives | How often |
|---|---|---|
| Postgres database | Your DATABASE_URL instance — all content, users, settings, tokens |
Daily at minimum; before every upgrade |
| Media files | Local uploads volume, or your object storage bucket | Daily (or rely on bucket versioning) |
| Environment secrets | JWT_SECRET, SECRETS_ENCRYPTION_KEY, database credentials |
Once, in a password manager — they rarely change |
SECRETS_ENCRYPTION_KEY is the one you cannot recover from a database dump.
It encrypts stored secrets (including bring-your-own AI keys) at rest — a
restored database without the original key cannot decrypt them, and they must
be re-entered by hand. Store it somewhere durable the moment you deploy.
Backing up Postgres
Any standard Postgres workflow applies. The simplest reliable loop:
# Nightly logical dump (content, settings, users, tokens — everything)
pg_dump "$DATABASE_URL" --format=custom --file=flowcms-$(date +%F).dump
# Keep 14 days, off the app host (S3, B2, a different machine)
- Railway: the Postgres plugin has point-in-time backups on paid plans —
turn them on, and still take a periodic
pg_dumpoff-platform. - Render: managed Postgres includes daily snapshots; the same advice applies.
- Docker: run
pg_dumpfrom the host or a sidecar cron container; don't keep the only copy on the same volume as the database.
Backing up media
- Object storage (recommended): if media lives in S3-compatible storage, enable bucket versioning and lifecycle rules — that is the backup.
- Local volume: snapshot or
rsyncthe uploads directory on the same schedule as the database, so content and its media restore to the same moment.
Restore procedure
- Provision a fresh instance (same image/version as the backup if possible — see Updating).
- Set the environment variables, including the original
SECRETS_ENCRYPTION_KEYandJWT_SECRET. - Restore the database:
pg_restore --clean --no-owner -d "$DATABASE_URL" <dump>. - Restore/point at the media storage (same bucket, or restored volume).
- Boot. Migrations reconcile the schema if the image is newer than the dump.
- Verify
/api/healthreturns ok, log in, spot-check an entry with images, and confirm integrations that use stored secrets still connect.
Rehearse the restore once on a scratch project. A backup you have never restored is a hope, not a plan — the rehearsal takes fifteen minutes and usually surfaces one missing env var.
Before every upgrade
Take a dump right before pulling a new image. Upgrades run forward migrations automatically; the dump is your rollback path to the previous version if you need one.