Running a site
Backups
What is backed up, when and where, how to restore on a live server or a new one, and what to check afterwards.
Every night at 3am the whole database is copied, compressed and kept in two places. This guide covers what that protects, and how Tidewater would get back after a bad day.
After reading this guide, you will know:
- What is backed up, and what is not.
- Where backups are kept, and how to turn on the offsite copy.
- How to restore on a server that is still running, and on a new one.
- What to check after a restore.
1. What is backed up
The database — every page, post, term, global, menu, asset record, user, form submission and redirect. It is one SQLite file, so a backup really is a complete copy of the content.
| Not backed up | Why |
|---|---|
| Uploaded files | they live in S3, which keeps its own copies — or on the storage volume, see below |
| The cache, job queue and cable databases | they rebuild themselves |
Help articles and other files in site/content/ |
they are in git, with the rest of the code |
| Your code | git |
Warning
If uploads are stored on the server’s volume rather than in S3, the nightly backup does not include them. Losing the server loses the images. Move uploads to S3 once the images matter.
2. How and where
The nightly job compacts the database into a clean, self-contained copy, gzips it, and keeps it:
- On the server, in
storage/backups/, for 7 days. The fastest way back after a bad migration or a mistake. - In S3, in the bucket named by
DB_SNAPSHOT_BUCKET, undercms_db_snapshots/<environment>/. This is what saves you if the server itself is gone.
Set DB_SNAPSHOT_BUCKET and DB_SNAPSHOT_REGION in the deploy to turn the offsite copy on — see
Deploying. Give the bucket a lifecycle rule, such as 30 days, so old copies expire.
Important
Without DB_SNAPSHOT_BUCKET, every boot warns that nothing is copied offsite. Take the warning seriously before
there is anything in the site you would mind losing.
Upgrades take their own snapshot too, before they change anything, and print the command that restores it. See Upgrading.
3. Restoring on a running server
Unpack the backup you want onto the storage volume, and look before you leap:
bin/kamal app exec 'gunzip -c storage/backups/2026-10-14.sqlite3.gz > storage/restored.sqlite3'
bin/kamal app exec 'sqlite3 storage/restored.sqlite3 "SELECT COUNT(*) FROM entries;"'
If the count looks right, stop the site, swap the files, and start it again:
bin/kamal app stop
bin/kamal app exec 'mv storage/restored.sqlite3 storage/production.sqlite3 && rm -f storage/production.sqlite3-wal storage/production.sqlite3-shm'
bin/kamal app boot
bin/kamal app exec starts a fresh container on the same volume, which is why it works while the site is stopped.
Caution
Remove the -wal and -shm files along with the old database. They belong to the file you replaced, and SQLite
would try to apply them to the restored one.
4. Restoring on a new server
Fetch the backup from S3 on your own machine, using the AWS keys in the credentials:
AWS_ACCESS_KEY_ID=$(bin/rails runner 'print Rails.application.credentials.dig(:aws, :access_key_id)') \
AWS_SECRET_ACCESS_KEY=$(bin/rails runner 'print Rails.application.credentials.dig(:aws, :secret_access_key)') \
aws s3 cp "s3://tidewater-backups/cms_db_snapshots/production/2026-10-14.sqlite3.gz" - --region eu-west-2 \
| gunzip > /tmp/restored.sqlite3
sqlite3 /tmp/restored.sqlite3 "SELECT COUNT(*) FROM entries;"
Deploy to the new server with bin/kamal setup, then stop the app, copy the file onto the server, move it into the
storage volume as storage/production.sqlite3 as above, and bin/kamal app boot.
5. When it starts again
Starting the application runs bin/rails nibble:prepare first. It applies any migrations newer than the backup and
checks the restored content against the schema of the running release. If it refuses, the message says why — a site
serving content it cannot understand is worse than a site that will not start.
5.1 Uploads and the database can disagree
The database is restored to last night; the files in S3 are from right now.
- Uploaded after the backup: the files are in the bucket but the database has no record of them, so
bin/rails nibble:assets:purge_unusedwould list them as unused. Do not run it with--confirmuntil you have decided whether they need adding back. - Deleted after the backup: the database points at files that are gone, so those images are broken. They can only come back if the bucket keeps old versions — turn versioning on before you need it.
6. Practise it
A backup nobody has restored is a hope, not a plan. Restore one onto a spare server on a quiet afternoon. You will learn how long it takes, which is the number you will want on the day it matters.