Skip to main content

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​

FormContents
flo backup create <id>pg_dump --clean --if-exists piped through gzip → flo-<timestamp>.sql.gz
flo backup create <id> --fullThe 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>.sh on 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​

CommandDescriptionKey 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 .sha256 sidecars, and pre-restore snapshots.
  • R2: <bucket>/daily/, <bucket>/weekly/, <bucket>/monthly/, with the matching .sha256 objects 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:

  1. DR guard — a tenant with a DR block (tenant.dr) is refused: remove replication and agent bindings first.
  2. Checksum verification — the .sha256 sidecar is verified when present; --require-checksum fails closed on legacy backups without one.
  3. Pre-restore snapshot — the current database is dumped to backupDir/restore-<ts>.pre.sql.gz before anything is overwritten.
  4. Single transaction — the restore runs inside one psql transaction; a mid-stream failure rolls back.
  5. Confirmation — destructive prompt unless -y.
  6. Full restore — the archive is decrypted and requires its matching database dump beside it (-full.tar.gz next to .dump or .sql.gz); both are checksum verified. --from-local supports 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.