I recently set up automated backups for a Rails application running SQLite in production. The setup uses Litestream to continuously replicate the database to Cloudflare R2 storage.

For those unfamiliar, Litestream streams SQLite changes to an S3-compatible bucket in near real-time. Cloudflare R2 works nicely here as it’s S3-compatible and has generous free tier limits. The litestream-ruby gem handles the integration with Rails and Puma.

Setting up Cloudflare R2

First we need a bucket and API credentials in Cloudflare.

Create a new R2 bucket. I’d recommend naming it something like your-app-name--backups to keep things organised.

Next, create a new “User API Token” with the following settings:

  • Name it whatever makes sense to you, something like Your App Name - Backups
  • Set permissions to “Object Read & Write”
  • Apply to specific buckets only and select your newly created bucket

Take note of the credentials that are generated:

  • The “Access Key ID” - looks like y43hqiefniu34y
  • The “Secret Access Key” - looks like enu3yb732yiu3yxeiuy23bbi7yb3uxyiyu
  • The “jurisdiction-specific endpoint” URL - looks like https://1a2b3c4d5e.r2.cloudflarestorage.com

Keep these safe, we’ll need them in the next step.

Storing credentials in Rails

Assuming you’re already familiar with Rails encrypted credentials, open the production credentials file:

EDITOR="code --wait" rails credentials:edit --environment=production

Add your Litestream credentials:

# config/credentials/production.yml.enc
# Note these credentials are completely made up - keyboard mashing
litestream:
  replica_bucket_name: your-app-name--backups
  replica_bucket_ext_url: 1a2b3c4d5e.r2.cloudflarestorage.com
  replica_key_id: y43hqiefniu34y
  replica_access_key: enu3yb732yiu3yxeiuy23bbi7yb3uxyiyu

Installing the Litestream gem

Follow the litestream-ruby setup:

bundle add litestream
rails generate litestream:install

This will generate the config files we need to modify.

Configuring Puma

Add the Litestream plugin to your Puma config so it starts automatically with your web server:

# config/puma.rb
# Run litestream only in production.
plugin :litestream if ENV.fetch("RAILS_ENV", "production") == "production"

Configuring Litestream

The generator creates a config/litestream.yml file that needs some adjustments for Cloudflare R2.

Remove any access-key-id or secret-access-key entries from the file - we’ll be setting these via the initializer instead.

- access-key-id: $LITESTREAM_ACCESS_KEY_ID
- secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY

Update the bucket values to use your bucket name:

- bucket: $LITESTREAM_REPLICA_BUCKET
+ bucket: your-app-name--backups

Add the endpoint configuration. This is needed because we’re using Cloudflare R2 rather than Amazon S3:

+ endpoint: $LITESTREAM_REPLICA_ENDPOINT

You can remove the production_cache and production_cable database configs unless you specifically want to back those up. I don’t see much value in backing up cache or cable databases.

The final config/litestream.yml should look like:

# config/litestream.yml
dbs:
  - path: storage/production.sqlite3
    replicas:
      - type: s3
        endpoint: $LITESTREAM_REPLICA_ENDPOINT
        bucket: your-app-name--backups
        path: storage/production.sqlite3
  - path: storage/production_queue.sqlite3
    replicas:
      - type: s3
        endpoint: $LITESTREAM_REPLICA_ENDPOINT
        bucket: your-app-name--backups
        path: storage/production_queue.sqlite3

Setting up the initializer

The generator also creates config/initializers/litestream.rb. Uncomment and update the following lines:

# config/initializers/litestream.rb
# Configuration condensed for brevity. 
Rails.application.configure do  
  # Configure Litestream through environment variables. Use Rails encrypted credentials for secrets.  
  litestream_credentials = Rails.application.credentials.litestream  
  
  ENV["LITESTREAM_REPLICA_ENDPOINT"] = litestream_credentials&.replica_bucket_url  
  config.litestream.replica_bucket = litestream_credentials&.replica_bucket_url  
  #  
  # Replica-specific authentication key. Litestream needs authentication credentials to access your storage provider bucket.  
  config.litestream.replica_key_id = litestream_credentials&.replica_key_id  
  #  
  # Replica-specific secret key. Litestream needs authentication credentials to access your storage provider bucket.  
  config.litestream.replica_access_key = litestream_credentials&.replica_access_key
end

The awkward bit here is the ENV["LITESTREAM_REPLICA_ENDPOINT"] line. We’re setting an environment variable because the config/litestream.yml reads from $LITESTREAM_REPLICA_ENDPOINT. For some reason the config.replica_endpoint option doesn’t work with Cloudflare R2, so this workaround gets the endpoint into the YAML config where it’s needed.

Ship it

There we have it, continuous SQLite backups streaming to Cloudflare R2. Once deployed, Litestream will start replicating your database changes automatically whenever Puma starts.

If you need to restore from a backup, the litestream-ruby gem provides rake tasks to help with that - check the gem documentation for details.