Backups (GFS on R2)
The Flo CLI implements a Grandfather-Father-Son (GFS) backup system for every tenant: PostgreSQL dumps (optionally full archives) uploaded to the tenant's Cloudflare R2 bucket, with retention enforced by bucket lifecycle rules.
R2 bucket (flo-<slug>-backups)
├── daily/ → kept 7 days
├── weekly/ → kept 30 days (Sundays)
├── monthly/ → kept 90 days (1st of the month)
└── <artifact>.sha256 sidecars
What a backup contains
| Form | Contents |
|---|---|
flo backup create <id> | pg_dump --clean --if-exists piped through gzip → flo-<timestamp>.sql.gz |
flo backup create <id> --full | The DB dump plus a flo-<timestamp>-full.tar.gz archive of the tenant directory (uploads, certs, .env) — backups/ and .flo-private/ are excluded. R2 media is downloaded first and packed as r2-media/ |
Full archives are encrypted in place with AES-256-GCM before upload. The file
layout is FLOBK1 | salt(32) | iv(16) | ciphertext | authTag(16); the key is
derived with scrypt from the operator's ~/.flo/master.key. Restore auto-detects
the magic header and decrypts transparently.
Every produced artifact gets a <artifact>.sha256 sidecar (coreutils
sha256sum shape), written before upload and copied to every tier alongside the
artifact. Restore verifies it; --require-checksum turns a missing sidecar into a
hard failure.
Scheduling
flo backup schedule <id> --bucket flo-<slug>-backups --cron "0 3 * * *" -y
flo backup schedule <id> --run-now -y # schedule + one immediate verifying run
flo backup schedule <id> --disable # remove the schedule
- Default cron:
0 3 * * *(daily at 03:00). The command writes~/.flo/scripts/flo-backup-<id>.shon the target host and appends one tagged crontab entry, logging to~/.flo/logs/flo-backup-<id>.log. Re-runs are idempotent — duplicates are collapsed and a double-cron guard re-checks the crontab after install. - The target bucket is created if missing and verified reachable before the schedule is installed, so a nightly run can never silently write nowhere.
- GFS tiers are computed per run: daily always, weekly on Sundays, monthly on the 1st (a Sunday the 1st gets all three).
- On production tenants (
isTest !== true) the schedule command also applies the canonical 7/30/90 lifecycle automatically; test clones keep nightly copies but no lifecycle unless applied manually.
The remote control-plane form accepts --bucket and --cron (validated), with
--run-now mandatory; set-bucket and retention run operator-local with --vps
because R2 write keys stay off the control plane.
Retention
Retention is enforced only by the bucket's lifecycle rules — the backup script never prunes.
flo backup retention [id] # read-only grading of every bucket (7/30/90)
flo backup retention <id> --apply # write the canonical GFS lifecycle
The canonical rules are daily/ → 7d, weekly/ → 30d, monthly/ → 90d, plus
abort-incomplete-multipart after 7 days. --apply requires an explicit instance ID
(no bulk writes) and a confirmation (or -y).
Command reference
| Command | Description | Key options |
|---|---|---|
flo backup create <id> | Create a DB dump | --full, --upload, --s3-* |
flo backup restore <id> | Restore a DB (or full) backup | --file, --from-local, --fresh, --no-owner, --no-restart, --require-checksum, -y |
flo backup ls <id> | List local + S3 backups with size and date | --s3-* |
flo backup download <id> | Download from S3 | --file <key>, --from-vps <profile>, --s3-* |
flo backup upload <id> | Upload a local file to S3 | --file <path>, --s3-* |
flo backup status [id] | Backup freshness (R2) + cron health, one or all tenants | --json |
flo backup retention [id] | Read/apply bucket GFS lifecycle | --apply, -y |
flo backup schedule <id> | Install/remove the GFS cron | --bucket, --cron, --run-now, --disable, -y |
flo backup set-bucket <id> <bucket> | Link a bucket to the instance |
S3 credentials resolve from --s3-* flags, FLO_S3_* env vars, or the stored
config.r2; the bucket from --s3-bucket, FLO_S3_BUCKET, or
tenant.backupBucket. flo backup download without --file opens an interactive
picker sorted by filename/date (newest first), not by tier prefix.
Where files live
- Local (target host):
~/.flo/instances/<id>/backups/—flo-<ts>.sql.gz,flo-<ts>-full.tar.gz, their.sha256sidecars, and pre-restore snapshots. - R2:
<bucket>/daily/,<bucket>/weekly/,<bucket>/monthly/, with the matching.sha256objects beside each artifact.
Verification
flo backup status # every instance in the resolved location(s)
flo backup status <id> # one instance; exits non-zero on fail
flo backup ls <id> # what objects actually exist
flo backup retention # is lifecycle retention actually enforced?
status answers two questions together: is there a fresh dump in R2 (real probe,
including media-bucket detection), and is exactly one backup cron scheduled. The
control-plane dashboard's Backups tab shows the same freshness through read-only
R2 credentials (see Control Plane).
Restore procedure
# Interactive picker (local + S3 backups)
flo backup restore <id>
# Explicit file; cross-tenant restores strip ownership
flo backup restore target --file /path/to/source_backup.sql.gz --no-owner
# Restore a local file onto a remote tenant
flo --vps production backup restore <id> --file ~/backups/flo-<ts>.sql.gz --from-local --fresh --no-restart -y
Safety checks built into restore:
- DR guard — a tenant with a DR block (
tenant.dr) is refused: remove replication and agent bindings first. - Checksum verification — the
.sha256sidecar is verified when present;--require-checksumfails closed on legacy backups without one. - Pre-restore snapshot — the current database is dumped to
backupDir/restore-<ts>.pre.sql.gzbefore anything is overwritten. - Single transaction — the restore runs inside one
psqltransaction; a mid-stream failure rolls back. - Confirmation — destructive prompt unless
-y. - Full restore — the archive is decrypted and requires its matching database
dump beside it (
-full.tar.gznext to.dumpor.sql.gz); both are checksum verified.--from-localsupports database backups only.
--fresh resets the target's public schema and drops source ownership/ACLs
(idempotent cross-tenant restore); it implies --no-owner. --no-restart leaves
application containers stopped after the attempt.
Relationship with the legacy rclone script
Automated Backups documents the legacy single-tenant
backup_db.sh script that rclone-copies dumps to Google Drive on a 6-hour cron.
That flow is standalone and has no GFS tiers, encryption, checksum sidecars, or
lifecycle retention. The CLI GFS system above supersedes it for managed
multi-tenant deployments; keep the legacy page only for the standalone/self-hosted
setup.