Skip to main content
Continuous, off-server backups of the production/staging SQLite databases are provided by Litestream via the litestream Ruby gem. The Litestream binary runs as a Puma plugin inside the app container and streams the WAL to S3-compatible object storage in near real time.

What is replicated

Litestream config lives in config/litestream.yml. Two databases are replicated: Deliberately not replicated: cache (Solid Cache — fully regenerable) and cable (Solid Cable — transient Action Cable messages). Both would be pure noise. storage/ is a persisted Kamal volume (voteringsapp_storage:/rails/storage in production, /volume1/docker/votering:/rails/storage on staging), so Litestream’s generation/temp files survive container restarts.

Enabling backups (operator, one-time per environment)

Litestream is ENV-gated and inert until a replica bucket is configured — the Puma plugin (config/puma.rb) only loads when LITESTREAM_REPLICA_BUCKET is set, mirroring the Sentry DSN-gating philosophy. Un-provisioned deploys, dev, and test are completely unaffected. To turn backups on, set these secrets in Doppler (project votering) and redeploy:
Set the equivalent secrets on the stg config for staging.

How it is wired

  • The Puma plugin (plugin :litestream in config/puma.rb) runs the Litestream binary inside the app container, once, from the Puma master.
  • config/litestream.yml is read raw by the Go binary, so it must not use ERB; it relies on Litestream’s own environment expansion (${RAILS_ENV}, $LITESTREAM_*).
  • config/initializers/litestream.rb feeds the LITESTREAM_* variables into the gem’s config.
  • The secrets reach the container through Kamal’s env.secret and Doppler (prd / stg). .kamal/secrets* read them with || echo "" fallbacks, so a deploy succeeds before the bucket is provisioned.

Checking backup status

Automated freshness monitoring

LitestreamVerificationJob runs daily at 04:00 UTC — 06:00 Oslo time in summer, 05:00 in winter; the schedule names UTC, so the app zone does not move it (config/recurring.yml) — and calls Litestream.verify! against each replicated database. On failure or staleness it reports to Sentry (Sentry.capture_exception) and the Rails log (Rails.logger.error), so a broken/stale backup is observable. The job no-ops when Litestream isn’t configured.

Restore runbook

Restoring rebuilds a database file from its replica. Do this on the server (the config + credentials live there).
Point-in-time recovery is supported via -timestamp on the restore command; see the Litestream restore reference.
Restore rehearsal is mandatory before relying on backups. A backup you have never restored is not a backup. After enabling Litestream in an environment, perform at least one restore rehearsal against a scratch environment (never production): provision a throwaway bucket + deploy, let replication run, then restore to a temp path and diff against the live db. This rehearsal requires a real bucket, credentials, and a deploy, so it is an operator step that cannot be automated from the app codebase.
Last modified on October 21, 2026