Backups workflow
This guide walks the everyday backup commands. The mental model behind them — what a backup contains per engine, the three trust states, what backups guarantee — lives in Backups (concept). Read that page first if any of those terms are new.
What you will learn
- How to create, list, verify, and download backups from the CLI
- How to restore a backup (engine-specific mechanics)
- The pre-destructive workflow pattern most projects need
- How to wire backups into CI before staged deploys
- How to read trust labels and recover when they say something is wrong
Prerequisites
- The Flux CLI installed and logged in (
flux login). - A project's
slugand seven-characterhashfromflux list(or aflux.jsoncarrying both — see Configuration). - Disk space for the artifact when you download —
pg_dump -Fcarchives are compressed but can still be substantial for active projects.
1) Create a backup
Both engines use the same command. The control plane runs pg_dump server-side and stores the artifact in its own backup volume:
flux backup create --project bloom-atelier --hash 0a1b2c3On v1 dedicated the artifact is the full project database. On v2 shared it is the tenant API schema only (pg_dump -Fc --schema=t_5ecfa3ab72d1_api --no-owner --no-acl). Both run in the same command shape; the engine determines the contents.
Creating a backup does not make it trustworthy. The fresh artifact is in the artifact validated state at best — the file exists and looks structurally fine. Promoting it to restore-verified is a separate step (see 3).
A common shortcut is to chain create and verify:
flux backup create --project bloom-atelier --hash 0a1b2c3 \ && flux backup verify --project bloom-atelier --hash 0a1b2c3 --latest
This is the single most useful one-liner before any destructive action.
When the operator has enabled R2 offsite replication, flux backup create also reports offsite upload status. Offsite storage is not a substitute for restore verification — always run flux backup verify before destructive work. Retention that removes a restore-verified local row also deletes that backup's R2 object (or filesystem offsite copy). Historical orphans uploaded before this alignment may still need a one-time bucket cleanup.
Self-hosted operators can optionally email backup-scheduler failures (freshness pipeline, offsite, artifact validation, tick hard-fail) via Resend (primary) or SMTP (fallback). The same env can page on host/Docker errors when FLUX_OPS_WATCH_ENABLED=1. See Email alerts for scheduler / ops failures. Unset send provider is a no-op and does not affect backups. Cloudflare Email Routing cannot send outbound mail.
2) List backups
flux backup list --project bloom-atelier --hash 0a1b2c3The output shows recent backups newest-first with their trust labels (per the three trust states). A status block at the top summarizes the latest backup, verification state, and whether destructive actions are allowed:
Latest backup: 1781975913697 · 2026-06-20T17:18:33.697Z · Jun 20, 2026, 5:18 PM UTC · 2 days agoVerification: Restore-verifiedSafe destructive actions: Allowed
Timestamps in CLI output always show Unix ms · ISO UTC · human UTC · relative so machine and operator forms stay aligned (see docs/ARCHITECTURE-CONTRACT.md § CLI timestamps).
Common trust labels in the history list:
- Restorable — restore-verified, safe to act on.
- Created, not restore-verified — file exists and looks valid; nobody has tried to restore it yet.
- Restore verification failed — the artifact exists but
pg_restoredid not reproduce a usable database. Treat as broken. - Validating backup artifact — the upload finished moments ago and the validator is still catching up. Wait briefly and re-list.
- No backups — exactly what it says; create one.
Pass --verbose for catalog timestamps, artifact paths, and the underlying tier names. Useful when a label looks wrong and you need to trace it.
List output also includes platform minimum backup freshness when the control plane exposes it: whether the newest restore-verified backup is within the platform interval (default seven days). Overdue freshness is informational — destructive actions still use the restore-verified gate on the latest backup, not freshness alone.
3) Verify a backup
This is the only step that promotes a backup to restorable. Verification runs pg_restore against the artifact in a disposable Postgres container on the control plane and checks that the tenant (or dedicated) schema came back. Before that restore, Flux creates NOLOGIN stubs with no superuser and no BYPASSRLS for every non-reserved role the archive names — including t_<shortId>_role and t_<shortId>_ddl — so CREATE POLICY ... TO t_<shortId>_ddl and ALTER ... OWNER TO can be applied. The container is removed when verification finishes, which drops the stubs. pg_restore must exit 0 with no ignored errors; a missing role is not skipped. A non-empty dump must restore at least one non-system table. A schema-only empty v2 tenant export (zero user tables) is still restore-verified when the expected t_<shortId>_api schema is present after restore and the dump TOC lists that schema with no TABLE entries — emptiness has to match the dump, not look like a failed restore:
flux backup verify --project bloom-atelier --hash 0a1b2c3 --latestVerify the latest with --latest, or a specific row with --id <backupId>. If verification fails, the row's trust label flips to Restore verification failed and the underlying error is recorded in the catalog — flux backup list --verbose will show it.
Restore verification is the contract. A backup that has never been verified is, operationally, not a backup yet. The pre-destructive pattern in Section 6 is built around this idea.
4) Download a backup
flux backup download --project bloom-atelier --hash 0a1b2c3 --latest -o ./bloom.dumpPass --latest (newest backup) or --id <backupId> (a specific row). The artifact is binary (pg_dump -Fc); the CLI refuses to write it to a terminal without -o or a redirect to avoid garbling the shell:
# Equivalent — redirect instead of -oflux backup download --project bloom-atelier --hash 0a1b2c3 --latest > ./bloom.dump
The downloaded file is a standard Postgres custom-format archive. pg_restore --list ./bloom.dump works against it without any Flux-specific tooling.
5) Restore a backup
There is no flux backup restore command today. Restoring is a two-step manual flow on both engines, deliberately so — the engines have different restore targets:
v1 dedicated — restore into the project's own Postgres
Get the project's Postgres connection string from the CLI:
flux project credentials bloom-atelier --hash 0a1b2c3# Copy the Postgres URI under the "Postgres" section.
Run pg_restore from your machine:
pg_restore --clean --if-exists --no-owner --no-acl \ --dbname "<paste Postgres URI>" \ ./bloom.dump
--clean --if-exists drops existing objects before recreating them. Skip those flags if you want to overlay onto an empty database instead.
v2 shared — restore into a Postgres you control
A v2 backup is a tenant export, not a shared-cluster restore. The intended targets are:
- A v1 dedicated stack you are migrating to (
flux migratedoes this for you — see Pooled → dedicated migrate). - Your own Postgres for archival, off-platform analysis, or running locally.
The restore is a normal pg_restore against any Postgres with the same major version; the dump is portable:
pg_restore --no-owner --no-acl \ --dbname "postgresql://user:pass@host/your_database" \ ./bloom.dump
pg_dump --no-acl omits GRANT and ALTER DEFAULT PRIVILEGES from the archive. pg_dump --no-owner does not remove ownership from a custom-format archive; pg_restore --no-owner is what suppresses ALTER ... OWNER TO. CREATE POLICY ... TO <role> is neither an owner command nor an ACL, so it is always in the file. Flux backup verification passes --no-owner --no-acl and still has to stub those policy roles. A restore that does not pass --no-owner (including flux db restore on v1, and a manual pg_restore without that flag) also applies OWNER TO t_<shortId>_ddl and needs the same stub.
Before pg_restore into a database that does not already have the tenant roles, create them as NOLOGIN with no superuser and no BYPASSRLS. At minimum: t_<shortId>_role and t_<shortId>_ddl (same short id as t_<shortId>_api), plus anon, authenticated, service_role, and authenticator if the archive names them. Leave the stubs in place on a database you will serve: the restored policies reference them. Re-create grants before serving traffic when the archive was taken with --no-acl, because those grants were not written into the file. flux db restore (v1 dedicated) and v2→v1 migrate create these stubs themselves. flux db restore does not pass --no-acl or --no-owner, so policies and any ownership commands in the archive are kept.
6) Pre-destructive workflow pattern
The most common reason to use Flux backups in day-to-day work is "back up before doing something risky." The pattern is the same regardless of engine:
# 1. Create + verify in one shotflux backup create --project bloom-atelier --hash 0a1b2c3 \ && flux backup verify --project bloom-atelier --hash 0a1b2c3 --latest# 2. Confirm the latest is restore-verifiedflux backup list --project bloom-atelier --hash 0a1b2c3 | head -3# 3. Do the risky thing — apply a migration, drop a column, run a destructive scriptflux push ./db/migrations/0042_drop_legacy_table.sql# 4. If something is wrong, the verified backup is the recovery path
This pattern is the same one V1 dedicated quick SQL and Pooled → dedicated migrate link to. It is not engine-specific; it is destructive-action-specific.
flux nuke enforces the pattern by default — it refuses to remove a project unless the latest backup is restore-verified. The override (--skip-backup-check) exists for cases where you genuinely want to destroy without a recovery path; the warning text on stderr makes the choice explicit.
7) CI pattern: backup before staged deploy
Wiring this into CI is straightforward. The shape that works for most teams:
# Pseudocode — adapt to your CI runner- name: Backup production project run: | flux backup create --project ${{ vars.FLUX_PROJECT_SLUG }} --hash ${{ vars.FLUX_PROJECT_HASH }} flux backup verify --project ${{ vars.FLUX_PROJECT_SLUG }} --hash ${{ vars.FLUX_PROJECT_HASH }} --latest- name: Apply migrations run: flux push ./db/migrations/# If migration fails, the verified backup is recoverable.
Keep the API token used here narrow — read-write to the projects it touches, nothing more. See Project secrets.
Common pitfalls
| Symptom | Likely cause |
|---|---|
flux backup verify consistently fails | Postgres major-version mismatch between the verify image and the source; or the tenant schema was deleted between create and verify. role "t_<shortId>_ddl" does not exist (or t_<shortId>_role) means the control plane is older than the tenant-role stub — upgrade flux-web, then flux backup verify --latest on the existing artifact. Schema-only empty v2 tenants should verify — if you still see no user tables found after pg_restore, the control plane needs the empty-tenant verify policy. |
| List always shows "Validating backup artifact" | The validator is stalled or the upload truncated; re-create and watch --verbose for the artifact size |
| Download writes nothing to disk | Forgot -o or a shell redirect on a TTY (the CLI refuses binary to terminal) |
flux nuke refuses with "not restore-verified" | Latest backup is in a non-restorable trust state; create + verify, or pass --skip-backup-check if you really mean to destroy without a recovery path |
| Old backup artifact missing on disk | Retention swept it; older rows can have valid metadata but no file. Check the project's retention window in the dashboard |
failed or restore_failed row disappears | The hourly retention pass visits every non-excluded project and removes those rows and their files once a newer restore-verified backup exists. It does not delete the newest backup. A failed create (including strict offsite failure) records validation and restore verification as skipped |
The full symptom-by-symptom map for backup failures lives in Troubleshooting.
Next steps
- Backups (concept) — what backups guarantee and what they do not
- V1 dedicated quick SQL — pre-destructive pattern in context
- Pooled → dedicated migrate — how
flux migrateproduces and consumes backups - CLI reference — every backup flag in detail
- Troubleshooting — restore failures and the destructive-action gate