Backing Up SQLite Databases for Rails 8 Apps Deployed with Kamal
SQLite has become a very practical production database for small and medium Rails applications. Rails 8 makes that path even more interesting because the default stack can use SQLite not only for the primary database, but also for cache, queue, cable, and other internal data stores.
That simplicity has one operational requirement that should not be postponed: backups.
I have a couple of Rails apps deployed with Kamal, and both store their production SQLite files in the persistent storage volume mounted by Kamal. I wanted a small tool that could do one thing well: connect to the production server, copy the SQLite files into my local project workspace, and keep only the most recent backups.
That tool is rails-kamal-sqlite-backups.
The Directory Pattern
The first decision was where backups should live.
My original script used one global directory:
~/DB_Backups
That worked for one app, but it was not a good long-term pattern. I want each product workspace to be self-contained. The app code, related worktrees, local scripts, and backups should live near each other.
The new pattern is:
~/Projects/<app>-workspace/
<app>/
db_backups/
<app>/
2026-05-23T020000-0600/
production.sqlite3
production.sqlite3-wal
production.sqlite3-shm
For a project called billing, that becomes:
~/Projects/billing-workspace/db_backups/billing/
This makes the backup location predictable for every future app.
Project Config Files
Each app gets a local config file under config/projects.
APP_NAME=billing
WORKSPACE_DIR=~/Projects/billing-workspace
LOCAL_BACKUP_ROOT="$WORKSPACE_DIR/db_backups"
SERVER_USER=deployer
SERVER_HOST=app.example.com
REMOTE_BACKUP_DIR=/storage
SSH_KEY=~/.ssh/billing
RETENTION_DAYS=7
The important part is that LOCAL_BACKUP_ROOT points to the workspace, not to a global backup folder.
The final destination is built by the script:
LOCAL_BACKUP_DIR="$LOCAL_BACKUP_ROOT/$APP_NAME"
So the same script can back up different apps without changing the script itself.
How It Maps to Kamal
In my Kamal projects, production storage is mounted like this:
volumes:
- "/storage:/rails/storage"
Inside the Rails container, the app sees the files under:
/rails/storage
On the host, those same files live under:
/storage
That is why the backup config uses:
REMOTE_BACKUP_DIR=/storage
The app host and SSH user come from config/deploy.yml too:
servers:
web:
- app.example.com
ssh:
user: deployer
Those values become:
SERVER_USER=deployer
SERVER_HOST=app.example.com
Running a Backup
If the project uses a dedicated deploy key, I load it first:
ssh-add ~/.ssh/billing
Then I run:
./backup.sh config/projects/billing.env
The script creates a timestamped local directory and uses rsync to copy only SQLite files:
rsync -avz \
--include='*.sqlite3' \
--include='*.sqlite3-*' \
--exclude='*' \
"$SERVER_USER@$SERVER_HOST:$REMOTE_BACKUP_DIR/" \
"$TARGET_DIR/"
The *.sqlite3-* include matters because SQLite can have companion files such as WAL and SHM files.
Keeping the Last Seven Days
Backups are useful until they become unmanaged disk usage. The cleanup script uses the same project config:
./cleanup.sh config/projects/billing.env
By default, it keeps seven days:
RETENTION_DAYS=7
The cleanup is intentionally narrow. It only removes timestamped directories directly under:
$LOCAL_BACKUP_ROOT/$APP_NAME
That protects the rest of the workspace from accidental deletes.
Automating It with Cron
For a daily backup and cleanup:
0 0 * * * ~/Projects/rails-kamal-sqlite-backups/backup.sh ~/Projects/rails-kamal-sqlite-backups/config/projects/billing.env
0 1 * * * ~/Projects/rails-kamal-sqlite-backups/cleanup.sh ~/Projects/rails-kamal-sqlite-backups/config/projects/billing.env
Each app gets its own config file and its own cron pair. No copied scripts. No hardcoded app logic.
What I Like About This Setup
This is not a backup platform. It is just a small script pair with a config convention.
That is the point.
The moving parts are easy to inspect:
- Kamal stores production SQLite files in
/storage. - The backup script copies SQLite files from that storage directory.
- The local destination lives inside the project workspace.
- The cleanup script keeps the last seven days.
- Real project configs stay out of git.
For my current projects, this is enough structure to be reusable without turning the backup process into infrastructure I have to maintain.