Chapter 8 — Production SQLite
Goal: make the one SQLite file safe to run in production — correct under concurrency, and backed up off the box. You'll have run:
rask new Shop— snapshots and continuous backup are both standard
Everything so far — products, orders, jobs, mail, cache, outbox — lives in a single app.db. That's the
One Person Framework bet: SQLite is your production database. Two things make that safe: the right
connection pragmas, and backups that don't live on the same machine.
1. The production pragmas
SQLite's defaults are tuned for a single embedded process — no WAL, no busy_timeout, foreign keys off —
so concurrent web requests hit database is locked. Rask.SQLite applies a tuned pragma set (WAL, a
busy-timeout, foreign_keys=ON, and more) to every connection.
You already have this. RaskApp opens the database with UseRaskSqlite rather than UseSqlite, because
Rask:Database:Provider is sqlite unless you say otherwise. The file is Rask:ConnectionStrings:App in
appsettings.json — Data Source=app.db while you develop — and there is nothing about it in Program.cs.
UseRaskSqlite is a drop-in for UseSqlite that also installs the pragma interceptor, so every background
processor (jobs, mail, outbox), every page, and every Product.Where(…) and every command handler shares a
connection that won't spuriously fail under load. StrictTables makes SQLite enforce each column's declared
type rather than quietly storing the text "lots" in an INTEGER column — see
STRICT tables. An app that writes its own
context gets the same by registering it with UseRaskDatabase(sp) (Rask.Data).
See production SQLite for the full pragma table, the load-test numbers, and the non-blocking write-retry story.
2. Snapshots — the cheap half
Scheduled point-in-time backups are a battery, on like the rest, and tuned in appsettings.json beside the
connection string:
"Rask": {
"Snapshots": {
"DestinationDirectory": "snapshots",
"Interval": "06:00:00",
"Retain": 7
}
}
It snapshots the database behind Rask:ConnectionStrings:App, so there is no second path to keep in step
with the first.
These go through SQLite's Online Backup API, not a file copy. That distinction is the whole value: with
WAL on, cp app.db backup.db can capture a torn database, because the committed data you want is split
between the file and the -wal. The backup API reads through a connection and gets a consistent image of a
live database. No external binary, no credentials.
3. Litestream — the off-box half
Snapshots on the same disk protect you from a bad migration, not from losing the disk. That's what
continuous backup is for: Litestream, run as a managed background service that
streams every change off the box to object storage (S3, GCS, Azure Blob, or a file target). It is off
until Rask:Litestream:ReplicaUrl names a replica — the scaffolded appsettings.json leaves it empty — so
rask dev works on a laptop with no litestream binary and no cloud credentials.
Set it, and RaskApp does two things:
- It restores first, before anything opens the database — a no-op when
app.dbis already there. Restore is skipped once the file exists, so doing it any later means a fresh machine quietly starts with an empty database instead of your data. This is the ordering that is easy to get wrong by hand. - It replicates the database behind
Rask:ConnectionStrings:App, reading the rest ofRask:Litestreamfor its settings.
The csproj sets RaskLitestreamDownload=false: the binary belongs in the Docker image, which the scaffolded
Dockerfile copies it into, rather than being fetched during everyone's build.
Set the replica when you deploy:
rask deploy --env "Rask:Litestream:ReplicaUrl=s3://my-bucket/shop"
Now the box is disposable: if it dies, a fresh box restores app.db from the replica on startup and
keeps going. That's what makes "one server" safe rather than scary — durability doesn't depend on that one
machine.
Verify
- A WAL file (
app.db-wal) appears next toapp.db, and under concurrent writes you no longer seedatabase is locked. - A
.dbfile lands in the snapshot directory on the configured interval. - With Litestream configured against a real bucket, deleting
app.dband restarting restores it from the replica (watch the startup log). - Your
/opspage (chapter 10) readsjournal_modeandforeign_keysback from the live connection and counts the snapshots on disk.
Learn more: production SQLite · Rask.Data