#Backups
Bay snapshots every app's SQLite database to an S3-compatible bucket on a schedule, verifies each snapshot, prunes old ones, and restores the latest one on its own when an app is redeployed onto an empty machine.
Backups cover the database and nothing else, and every backup response says
so in words. Uploads and .env are never archived: uploads are made durable by
putting them in a bucket, and the secrets a project declares come from the
deploy. A backup system's worst failure is somebody believing it covers more
than it does.
| Data | Covered | How it survives the host |
|---|---|---|
| the app's SQLite file | yes | scheduled snapshots in the backups bucket |
storage/ (uploads) |
no | bay config s3:apps + bay storage migrate: uploads live in a bucket |
.env, secrets |
no | declared secrets are redeployed from the project (.env.<env>, alepha deploy); the rest is lost |
| a stack service | when it declares a hook | x-lore.backup, see Stacks |
| an image app | no database to back up | no host path is mounted: keep state in a stack service or a bucket |
#Where backups go
Bay knows two S3 targets, named by who uses them. These commands, and every
other bay command on this page, run on the host with BAY_SOCKET set, as in
Operating:
export BAY_SOCKET=/run/bay/control.sock
# where BACKUPS go; never handed to an app
BAY_S3_ACCESS_KEY=... BAY_S3_SECRET_KEY=... \
bay config s3:backups --endpoint https://<account>.r2.cloudflarestorage.com --bucket bay-backups --keep 14
# where hosted APPS put their uploads; given to every app that declares a bucket
BAY_STORAGE_ACCESS_KEY=... BAY_STORAGE_SECRET_KEY=... \
bay config s3:apps --endpoint https://<account>.r2.cloudflarestorage.com --bucket bay-uploads
Credentials come from the environment, never the command line, where ps
would show them to every user on the machine. --region defaults to auto
(right for R2); --keep is the number of snapshots kept per app, 14 by
default. Neither target needs a Bay restart: s3:backups applies from the next
backup, and s3:apps reaches an app's .env at its next deploy. That deploy is
refused for an app with files in storage/ until
bay storage migrate has run.
⚠️ Use two credentials. bay config s3 sets both targets from one key: it
is convenient, and any hosted app given that key can then delete every backup.
Bay warns when you use it, and bay status flags it for as long as it lasts.
Scope the backup key to the backup bucket alone.
#The schedule
bay serve takes a snapshot of every app every --backup-interval, 24 hours
by default (0 disables it). Until bay config s3:backups names a bucket the
schedule does nothing and logs nothing; bay status shows backup NEVER. Each
run:
- snapshots the live database, in the app's own runtime image so the SQLite that reads it is the one that wrote it;
- verifies the copy with
PRAGMA integrity_check; - gzips it and uploads it as
apps/<name>-<env>/db/<timestamp>.sqlite.gz; - prunes the app's snapshots down to
--keep.
A snapshot is compressed, not encrypted: whoever reads the bucket reads the database.
bay status shows each app's last backup and its age. It warns when the last
one is older than twice the interval, when no backup was ever taken, and
when the last attempt failed, with the reason. A once-late run does not warn;
a stopped schedule does. It also warns while scheduled backups are held after a
failed restore.
#By hand
bay backup myapp/production # snapshot + verify + upload, now
bay backups myapp/production # what is stored, newest first
bay restore myapp/production # the latest
bay restore myapp/production --key apps/myapp-production/db/20261101T030000Z.sqlite.gz
bay restore is destructive: it replaces the live database. The one it
replaces is not deleted but set aside beside it, as
app.db.before-restore-<timestamp> (with its WAL and SHM files), so a restore
of the wrong snapshot can itself be undone by hand.
#Disaster recovery
When Bay creates a database for an app, because the machine is new or its disk was lost, and a backup of that app exists, Bay restores the latest snapshot before the app starts. It never does so over an existing database.
So recovering from a lost host is:
- Install Bay on a new machine.
- Configure the same backups bucket (and apps bucket) with
bay config. - Point DNS at the new machine, and redeploy each app with
alepha deploy --env production.
Each app comes back with its data as of its last snapshot, the secrets the deploy carries and its uploads from the apps bucket. What does not come back:
APP_SECRET. It lives only in the instance's.env, and neither a deploy norbay env setcan write it, so the new host generates a new one and every user is signed out.- Values set with
bay env set. They were in that.envtoo: set them again. - Uploads that were on the old host's disk only, which is why the next section exists.
- Stacks. Recovery covers apps only. A stack's variables, routes and registry
credentials are in no bucket, and its dumps are never restored on their own:
put the variables and credentials back, run
bay stack deploy, add the routes, thenbay backup restoreeach service. - The Lore connector.
connector.jsonis in no bucket: runbay connector setagain.
A failed restore does not fail the deploy: the app starts on an empty database,
and nothing retries, because a database now exists. The deploy says so:
alepha deploy prints a warning, and bay deploy answers restore.warning.
Scheduled backups of that app are held, so the snapshot it missed stays the
latest, and bay status flags the hold. Run bay restore myapp/production to
bring it back, or bay backup myapp/production to keep the database the app has
now. Either one ends the hold. Bay holds only when a snapshot may exist: an app
with an empty bucket, or none configured, starts empty and backs up as usual.
#Moving uploads to a bucket
An app that stores files ($storage) writes them under storage/ on the host's
disk until an apps bucket is configured. Those files exist in one place, and
bay backup names them as not covered. To move them:
bay config s3:apps --endpoint ... --bucket bay-uploads # once per host
bay storage migrate myapp/production
migrate stops the app, copies its local files into the bucket, repoints it at
the bucket and starts it again, so the app is down for the length of the copy.
The local copies are kept for you to delete once you have checked the app.
Enable versioning on the bucket if you want point-in-time recovery of uploads:
object storage keeps the current blob, not yesterday's.