Skip to content

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, under cms_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_unused will list them as stray. Do not run it with --confirm until 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.