There’s been a lot of excitement lately in the Rails community. Rails World just wrapped up, which brought with it some really great annoucements for the future of the platform, as well as the release of Rails 7.1.
This most recent release had a few particular additions/improvements that really piqued my interest in regards to how I approach deployment of new Rails apps, specifically around the inclusion of a standard Dockerfile as well as a focus on making SQLite a first-class citizen/consideration for production apps.
Before we continue, here are a few things you’ll want to make sure you’ve got setup/configured properly:
- Your app is using Rails 7.1 and has the default Dockerfile available to it
- Docker is installed on your local computer
- You have an account with a hosting provider like DigitalOcean or Hetzner
SQLite + Litestack
For this project I wanted to host my project on the cheapest DigitalOcean droplet possible, but it included some advanced requirements/features, including ActiveJob and ActionCable for background tasks and Turbo Broadcasts. While this certainly could have been done in the more traditional way of installing a traditional database like PostgreSQL or MySQL and redis on the server, my goal was simplicity.
In contrast, leveraging a single technology like SQLite for all of these core aspects of the app would allow me to greatly simplify the hosting requirements, which is where Litestack comes in.
If you aren’t familiar, Litestack is a wonderful project with the following objective:
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.
Installing the gem is very straightforward, drop it in your
Gemfile and run the installation command
rails generate litestack:install.
I’ll admit it, I’m late to the Docker party, so there were many of the concepts I was (and am still)
unfamiliar with, but the abstraction Kamal provides and the default Rails
Dockerfile are just
enough to allow me to get the job done.
That being said I do think there’s value in more broadly understanding Docker as well as things like Traefik, which is something that’s mentioned many times in Kamal’s docs, but is never really explained. I can piece together that it’s an orchestration and monitoring layer for your Docker containers, but I’ve found it difficult to learn much more about it and how to use it, so if you know of any good resources on better understanding it, please share.
The main gotcha I ran into was around how to store the SQLite databases in persistant storage.
I solved this by configuring a Kamal volume
and adding the following environmental variable to my
After a decent amount of trial and error, and losing my database between deploys more than once 😆, the following deployment config is what ended up getting the job done.
# deploy.yml service: url-shortner image: kylekeesling/url-shortner volumes: - "storage:/rails/storage" servers: web: hosts: - 18.104.22.168 options: "add-host": host.docker.internal:host-gateway labels: traefik.http.routers.rails_recipes.entrypoints: websecure traefik.http.routers.rails_recipes.rule: Host(`links.passtesting.com`) traefik.http.routers.rails_recipes.tls.certresolver: letsencrypt registry: username: kylekeesling password: - KAMAL_REGISTRY_PASSWORD env: clear: secret: - RAILS_MASTER_KEY - LITESTACK_DATA_PATH traefik: options: publish: - "443:443" volume: - "/letsencrypt/acme.json:/letsencrypt/acme.json" args: entryPoints.web.address: ":80" entryPoints.websecure.address: ":443" entryPoints.web.http.redirections.entryPoint.to: websecure entryPoints.web.http.redirections.entryPoint.scheme: https entryPoints.web.http.redirections.entrypoint.permanent: true entrypoints.websecure.http.tls: true entrypoints.websecure.http.tls.domains.main: "links.passtesting.com" certificatesResolvers.letsencrypt.acme.email: "email@example.com" certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json" certificatesResolvers.letsencrypt.acme.httpchallenge: true certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web