## The command that is safe to run while the app is serving

```bash
bin/kamal app exec 'sqlite3 storage/production.sqlite3 ".backup /tmp/backup.sqlite3"'
ssh your-server 'docker cp $(docker ps -qf name=web):/tmp/backup.sqlite3 ~/backup.sqlite3'
scp your-server:~/backup.sqlite3 ./backup-$(date +%F).sqlite3
```

Three steps because there are three boundaries: inside the container, on the host, and off the
machine. The third one is the one people skip, and it is the one that matters.

## Why cp is not good enough

SQLite writes in pages, and a write-ahead log holds changes that are not yet in the main file. A
`cp` catches whichever pages happened to be flushed at that instant, plus a `-wal` file you probably
did not copy. The result opens without complaint and is missing recent writes, or is corrupt in a
way you discover during a restore at the worst possible moment.

`.backup` uses SQLite's own backup API, which takes a consistent snapshot across both files.

## Verify the backup, because an unverified backup is a guess

```bash
sqlite3 backup-$(date +%F).sqlite3 'PRAGMA integrity_check;'
sqlite3 backup-$(date +%F).sqlite3 'SELECT COUNT(*) FROM accounts;'
```

`integrity_check` should print `ok`. A row count you recognize tells you it is the right database and
not an empty one, which is the other failure people discover late.

## Make it happen without you

A backup you run by hand happens until the week you are busy. Put it in a job and schedule it with
Solid Queue: a class under `app/jobs`, listed in `config/recurring.yml`. The same container runs it,
so there is nothing new to deploy. Upload the result to object storage with the S3 credentials that
`config/storage.yml` already reads.

## What this does not cover

Uploaded files. They live on the same volume but are not in the database, so a database backup
misses them entirely. If you are storing attachments on local disk rather than S3, back up `storage`
as well.
