Skip to content

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_dump off-platform.
  • Render: managed Postgres includes daily snapshots; the same advice applies.
  • Docker: run pg_dump from 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 rsync the uploads directory on the same schedule as the database, so content and its media restore to the same moment.

Restore procedure

  1. Provision a fresh instance (same image/version as the backup if possible — see Updating).
  2. Set the environment variables, including the original SECRETS_ENCRYPTION_KEY and JWT_SECRET.
  3. Restore the database: pg_restore --clean --no-owner -d "$DATABASE_URL" <dump>.
  4. Restore/point at the media storage (same bucket, or restored volume).
  5. Boot. Migrations reconcile the schema if the image is newer than the dump.
  6. Verify /api/health returns 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.