Deploy Rails with SQLite, Litestack and Litestream to Linode using Kamal

January 01, 2024

Rails is definitely getting better and better and now the ability to deploy apps with SQLite in production - all on a single node - will make apps faster, more efficient and more cost effective.

We’ve seen Rails apps deployed with SQLite in the past but there was always that “what if” lingering question. What if there’s a disaster and the database is lost? Welcome Litestream to the chat! Litestream was the missing link to ensuring against data loss.

After working on my own deploys for several hours and seeing so many folks ask about step by step tutorials, I decided to map it out from start to finish in hopes that it will help someone else. I also must say thanks to many other folks who helped me piece this together, most especially Dave Kimura and Drifting Ruby. I can promise you the membership to his site is well worth it.

A couple of notes about the services I am using:

  • Cloudflare - for domains and DNS administration
  • Linode - for both the node and object storage
    • A single node to run the app
    • An S3 compatible object storage account
  • Docker Hub and Docker Desktop
    • You have to have Docker Desktop running for Kamal deploys
    • You also have to have a registry where the Docker image gets loaded for your server to pull from, that’s where Docker Hub comes in.
    • Docker Hub is free but you only get one private repository. If you need more than one, you will have to pay for the Pro version at $60 a year.

Now let’s go through the steps.

1. Create a Linode

Sign up for a Linode account if you don’t have one and then create a Linode. If you’re using a service like Digital Ocean, this would be the same as creating a Droplet.

For this tutorial, these are the options I am using:

  • Debian 12 for my Image (Linux Distribution)
  • Region: Chicago (You should use the region closest to you or your audience)
  • Shared CPU Nanode 1GB - which costs $5 a month. I have successfully deployed my app on this lowest tier with no issue. If your app grows, you can easily resize but this is a good start.
  • Linode Label - set to your desired name
  • Root Password - pick a password
  • SSH Keys - I highly recommend you have this set up. My key is already set up with Linode, so I just have to check the box to add my existing key. You will need your SSH Key set up to follow this exactly.
  • Then at the bottom, click Create Linode.

2. Create an Object Storage Bucket

In your Linode account, you will see the various services offered on the left hand side, look towards the lower half and go into the Object Storage menu and then create a bucket.

For the options:

  • Give the label any desired name
  • Region: I would select the same region as your Linode instance.
  • The defaults on the bucket should have Access Control set to Private and CORS enabled - which will allow your Rails app to access it with the correct credentials.

3. Configure your DNS

  • Set the A record for yourdomain.com to the IP address of your Linode
  • If you also want to use WWW, make sure you have CNAME for WWW that has yourdomain.com as the content
  • If you are using a subdomain, create an A record with the name of the subdomain such as “app” and point it to the Linode IP. In this case, you would resolve to “app.yourdomain.com”. But this tutorial will focus on the main domain and “www”.

4. Setup the server and harden it

You will need to access the command line in your server from your terminal.

ssh root@<your server IP>

Then run the following commands, including creating the acme.json file which will allow letsencrypt to later store the certificate for https:

touch acme.json
chmod 600 acme.json
apt update && apt upgrade -y #keep the current configuration file when prompted
apt install fail2ban ufw -y
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw deny http
ufw allow https
ufw enable

5. Create Rails app with Litestack

About Litestack: Litestack is a Ruby gem that provides both Ruby and Ruby on Rails applications an all-in-one solution for web application data infrastructure. It exploits the power and embeddedness of SQLite to deliver a full-fledged SQL database, a fast cache , a robust job queue, a reliable message broker, a full text search engine and a metrics platform all in a single package.

rails new myapp -c tailwind

You don’t have to use Tailwind, I just do it by default now on all my projects.

Add the Litestack gem to your Gemfile.

bundle add litestack
rails generate litestack:install

This will update a number of files including your cable.yml to use litecable **and your **database.yml to use Litestack’s litedb adapter to wrap around SQLite.

6. Set up configuration

Go into config/envrionments/production.rb and use these settings where you see the config for cache store:

#config/environments/production.rb

config.cache_store = :litecache, {path: Rails.root.join("db/production/production_litecache.sqlite3")}
config.active_job.queue_adapter = :litejob

Create a yml file for litejob to add more configuration at config/litejob.yml.

#config/litejob.yml

queues:
    - [default, 1]
    - [important, 3]
    - [urgent, 5]
    - [critical, 10, "spawn"]

These are arbitrary names that you can use to specify the priority when queuing jobs. The numbers must be 1-10 with 10 being the highest priority.

Be sure to add the flag for assume ssl in your configuration too - or a Kamal deploy will not work. Look for force ssl and add this.

#config/environments/production.rb

config.force_ssl = true
config.assume_ssl = true

7. Let’s add Kamal

You can install Kamal globally with a gem install Kamal command. But I also add it to my Rails bundle.

bundle add kamal
kamal init

Once you run the kamal init command, it will create a config/deploy.yml file and a .env file.

Now let's add the environment variables to the .env file first.

KAMAL_REGISTRY_PASSWORD=change-this
RAILS_MASTER_KEY=change-this
LITESTACK_DATA_PATH=./db
LITESTREAM_DATABASE_PATH=/rails/db/production/data.sqlite3
LITESTREAM_REPLICA_URL=s3://your-bucket.your-region.linodeobjects.com/yourapp/db/production/data.sqlite3
LITESTREAM_ACCESS_KEY_ID=access-key-from-object-bucket
LITESTREAM_SECRET_ACCESS_KEY=access-secret-from-object-bucket

Some quick notes here:

  • The Kamal Registry Password can be obtained on Docker Hub. In your account, go to your profile in the top right corner, then click on My Account and then go to Security and generate an Access Token to use as your Password.
  • Rails Master Key comes from the master.key file in your Rails app.
  • The Litestack Data Path is arbitrary but I use "./db" and we will later set up a persistent volume for our database storage in the same structure as a typical rails app. So the database will live in the folder "/rails/db/" and Litestack will join the folder for "production" just like it would in development.
  • The Litestream Database Path is set to where the database will live after we do the next step of setting up the config/deploy.yml.
  • The Litestream Replica URL is how I set mine up to the S3 Bucket and folders I wanted to backup my database to. You can choose the folder structure you want but I wanted mine to be identical to the production environment.
  • The Litestream Access Key and and Secret Access are obtained from your object storage. Use an existing pair or generate a new set of tokens specifically for this.

Now, set up the config/deploy.yml. You will see entries for volumes that will be persisted on the server allowing you to keep your database and storage, if you choose, without losing the data and assets after each deploy. Keep in mind that db and storage are folders already created by rails. You CAN create different folder names, but you will need to create and chown them in your Dockerfile to ensure the rails app can access it.

# config/deploy.yml

service: appname
image: dockerhubusername/appname
servers:
  web:
    hosts:
      - YOUR.SERVER.IP.HERE
    labels:
      traefik.http.routers.appname.rule: Host(`yourdomain.com`, `www.yourdomain.com`)
      traefik.http.routers.appname_secure.entryPoints: websecure
      traefik.http.routers.appname_secure.rule: Host(`yourdomain.com`, `www.yourdomain.com`)
      traefik.http.routers.appname_secure.tls: true
      traefik.http.routers.appname_secure.tls.certresolver: letsencrypt

registry:
  username: dockerhubusername
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
    - LITESTACK_DATA_PATH
    - LITESTREAM_DATABASE_PATH
    - LITESTREAM_REPLICA_URL
    - LITESTREAM_ACCESS_KEY_ID
    - LITESTREAM_SECRET_ACCESS_KEY

volumes:
  - storage:/rails/storage
  - db:/rails/db

traefik:
  options:
    publish:
      - 443:443
    volume:
      - /root/acme.json:/letsencrypt/acme.json
  args:
    entryPoints.web.address: ":80"
    entryPoints.websecure.address: ":443"
    certificatesResolvers.letsencrypt.acme.email: "[email protected]"
    certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
    certificatesResolvers.letsencrypt.acme.httpchallenge: true
    certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web

Next, we need to get Litestream to run every time the app gets deployed. So edit your Dockerfile and add this after the line to install packages needed for deployment.

# Install packages needed for deployment
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Add below to get the latest Litestream and run

# Litestream
ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz

Next create a yml file for Litestream at config/litestream.yml.

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

dbs:
  - path: $LITESTREAM_DATABASE_PATH
    replicas:
      - url: $LITESTREAM_REPLICA_URL       

This file pulls the remainder of the ENV variables set which will tell Litestream where to send your backups. I highly recommend you check out the Litestream website for all the different configurations. Also, I am only setting this up to replicate. If you need to ever restore, you will have initiate the restore command manually using the bash prompt from Kamal.

Now, we need to edit the Docker Entry Point file to start Litestream once the server starts and use the litestream.yml we provided.

So open bin/docker-entrypoint and edit to add the litestream directive like this:

#!/bin/bash -e

# If running the rails server then create or migrate existing database
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
  ./bin/rails db:prepare
  litestream replicate -config /rails/config/litestream.yml &
fi


exec "${@}"

Now back in terminal from your app, time to curate the server from Kamal.

kamal server bootstrap
kamal setup

On future deploys:

kamal deploy

If you need to reconfigure your domain in the deploy.yml or change anything else, you can reset the certificates and web traffic entry with the commands below.

kamal traefik remove
kamal traefik boot

Any questions or thoughts to make the process better, please comment below.

Comments

You must be logged in to comment.