Running a site
Backups
What is backed up and when, and exactly how to put it back.
Every night at 3am your whole database is copied, compressed and kept in two places. You do not have to set anything up for the local copy; the offsite copy needs one setting, and is worth doing before there is anything in the site you would mind losing.
What is backed up
The database — every page, post, taxonomy term, asset record, user, form submission and redirect. That is one SQLite file, so a backup really is a complete copy of your content.
What is not, and why:
| Not backed up | Why |
|---|---|
| Uploaded files (images, video, documents) | They live in S3, which keeps its own copies |
| The cache, job queue and cable databases | They rebuild themselves; there is nothing in them worth keeping |
| Your code | It is in git, which is already a backup |
How and where
The nightly job compacts the database into a clean, self-contained copy, gzips it, and keeps it:
- On the server, under
storage/backups/, for the last 7 days. This is the fastest way back after a bad migration or a mistake in the content. - In S3, in the bucket named by
DB_SNAPSHOT_BUCKET, undercms_db_snapshots/<environment>/, with a 30-day retention policy on the bucket. This is what saves you if the server itself is gone.
Set DB_SNAPSHOT_BUCKET and DB_SNAPSHOT_REGION to turn the offsite copy on — see Settings.
Upgrades take their own snapshot too, before they change anything, and print the command that restores it. See Upgrading.
Restoring when the server is still alive
Get a shell on the server — with Kamal, bin/kamal app exec -i bash — then:
cd /rails
gunzip -k storage/backups/2026-09-14.sqlite3.gz -c > /tmp/restored.sqlite3
# Look before you leap: does this copy have what you expect?
sqlite3 /tmp/restored.sqlite3 "SELECT COUNT(*) FROM entries;"
If the count looks right, stop the application, replace storage/production.sqlite3 with /tmp/restored.sqlite3
(or staging.sqlite3 on staging), and start it again.
Restoring when the server is gone
You need the backup from S3, and the AWS credentials from your encrypted credentials file:
export RAILS_MASTER_KEY=$(cat config/credentials/production.key)
ACCESS_KEY_ID=$(RAILS_ENV=production bin/rails runner 'print Rails.application.credentials.dig(:aws, :access_key_id)')
SECRET_ACCESS_KEY=$(RAILS_ENV=production bin/rails runner 'print Rails.application.credentials.dig(:aws, :secret_access_key)')
AWS_ACCESS_KEY_ID="$ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$SECRET_ACCESS_KEY" \
aws s3 cp "s3://$DB_SNAPSHOT_BUCKET/cms_db_snapshots/production/2026-09-14.sqlite3.gz" - --region ap-southeast-2 \
| gunzip > /tmp/restored.sqlite3
sqlite3 /tmp/restored.sqlite3 "SELECT COUNT(*) FROM entries;"
Then put it in place exactly as above.
What happens when it starts again
Starting the application runs bin/rails nibble:upgrade before the server accepts requests. It applies any
database and content migrations newer than the backup, then checks the restored content against the schema the
running release expects.
If it refuses to start, the message says why, and bin/rails nibble:check shows the same list on demand. This is
deliberate: a site serving content it cannot understand is worse than a site that will not start.
Uploads and the database can end up out of step
The database is restored to last night; the files in S3 are from right now. Two things can be mismatched:
- Uploaded after the backup. The files are in the bucket but the restored database has no record of them, so
bin/rails nibble:assets:purge_unusedwill list them as stray. Do not run it with--confirmuntil you have decided whether those uploads need adding back. - Deleted after the backup. The restored database points at files that have already been removed, so those images will be broken. They can only come back if versioning is enabled on the bucket — worth checking that it is, before you need it.
Practise it
A backup nobody has restored is a hope, not a plan. Restore one into staging on a quiet afternoon, before the day you need it. You will learn how long it takes, which is the number you will want when it matters.