Solid Cable in Production with Kamal
Solid Cable in Production with Kamal
Last week I wrote about Solid Cable and how amazingly easy it was to get up and running and add real time web socket magic to a Rails app. Rails 8 and the solid trifecta of Solid Cache, Solid Queue and Solid Cable, is a massive unlock to productivity, particularly evident when adding some of these historically pretty complicated capabilities and services: caching, job queues and web sockets respectively.
When I went to move this to production, I realised that there often aren’t that many blog posts or examples - including my own previous posts - that go into details beyond a demo on localhost. Lets fix that now! It was pretty straightforward, but a couple of tiny details caught me out, so wanted to capture those for next time!
Managing Multiple Databases in Rails
In the previous post we set up the “primary” and “cable” databases both with SQLite
in the development environment as that’s the default in Rails 8, and also the simplest.
But in my production app - and I suspect like many others - I was already using PostgreSQL as
my main “primary” database. So we need to add SQLite in production to run alongside - in
my case - Postgres. We configure database connections in config/database.yml
so open
this up and have a look at what you currently have.
In my case I had a &default
YAML anchor to set up the common configs, then a key each for
development
, test
and production
that pulls in the &default
config with a merge key
and adds the environment specific configs in each as needed. I had postgresql
set up for all three environments as shown below. You may have something different,
like sqlite3 or mysql as your primary, but the general idea is the same.
# Existing database.yml
default: &default
adapter: postgresql
encoding: unicode
port: 5432
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
host: <%= ENV["DB_HOST"] %>
database: <%= ENV["POSTGRES_DB"] %>
username: <%= ENV["POSTGRES_USER"] %>
password: <%= ENV["POSTGRES_PASSWORD"] %>
Ok. So now to use Solid Cable with SQLite while keeping PostgreSQL for the main app database we
need to update database.yml
to support multiple databases ie. SQLite as well as our existing
PostgreSQL. This capability was introduced in Rails 6 I think, and you can read all about it
in the excellent Rails Guides if you want to get some more background.
Firstly, lets add a new YAML anchor for sqlite to config/database.yml
as shown here. Put it
just below the existing &default
anchor block.
sqlite: &sqlite
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
When using multiple databases in an environment, we can specify them by adding a configuration
key for each under the environment key. Here’s how my database.yml
file looked after adding the
&sqlite
anchor and then adding primary
and cable
configurations for each environment:
default: &default
adapter: postgresql
encoding: unicode
port: 5432
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
sqlite: &sqlite
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
primary:
<<: *default
database: myapp_development
cable:
<<: *sqlite
database: storage/myapp_development_cable.sqlite3
migrations_paths: db/cable_migrate
test:
primary:
<<: *default
database: myapp_test
cable:
<<: *sqlite
database: storage/myapp_test_cable.sqlite3
migrations_paths: db/cable_migrate
production:
primary:
<<: *default
host: <%= ENV["DB_HOST"] %>
database: <%= ENV["POSTGRES_DB"] %>
username: <%= ENV["POSTGRES_USER"] %>
password: <%= ENV["POSTGRES_PASSWORD"] %>
cable:
<<: *sqlite
database: /data/myapp_production_cable.sqlite3
migrations_paths: db/cable_migrate
We’re only really interested in the “production” settings in the context of this post but I’ve left the rest in for completeness.
So, lets go over this line by line:
primary:
specifies the primary database. In our case that’s our existing “main” application database. It should always be named “primary” to keep things clear.<<: *default
is a YAML merge key that pulls in the configuration from the&default
anchorhost
,database
,username
andpassword
obviously are the location, name and auth credentials for your database. Best practice is to pass these in from environment variables as showncable
is the configuration key for our Solid Cable SQLite instance! It’s important that we name it “cable” so it matches with the name inconfig/cable.yml
<<: *sqlite
is another YAML merge key that pulls in the config from the&sqlite
anchordatabase
pay close attention here! this is the path to the SQLite database file in production. In my case I have this in a volume mounted by Kamal. More on that latermigrations_paths
is the path in our codebase where we can add migrations specific to the SQL database
Solid Cable Configuration
Nothing to change here but as a reminder, Solid Cable is configured in config/cable.yml
and
important as mentioned above that the production database configuration key for the SQLite
database we’re using for Solid Cable is named the same here. ie “cable”
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
Kamal SQLite Location
As I’ve written about numerous times in the past like here and here, I’m all in on Kamal! It’s a fantastic tool for deploying web apps. Previously I had only used PostgreSQL and only one database per app. Now we need to add SQLite to the mix. Fortunately it’s super simple, but I had one gotcha that took some fiddling to get right and wanted to capture here.
As you saw above, in the database.yml
file we need to specify the name and location of the
SQLite database file. The Rails default is to set this to database: storage/production_cable.sqlite3
which makes sense as a default.
In our case with Kamal, we want to make sure that this database persists across deployments, so we need to make sure it’s mounted on the server’s file system outside the docker container but still accessible from the container each time it’s deployed with Kamal.
Fortunately this is really easy too with the volumes
key in config/deploy.yml
. With this
setting, Kamal will automatically make a persistent storage volume at /data
on our server.
## config/deploy.yml
volumes:
- "myapp_data:/data"
Then we need to make sure we are using this volume in the database.yml
config so that Rails
knows where to find the file. In my case above it’s database: /data/myapp_production_cable.sqlite3
The embarrassing “gotcha” I mentioned above was I was missing /
in front of data
so Rails was
trying to put the database in the relative path /rails/data
and not the volume I had mounted
at /data
! Very annoying but didn’t take long to realise :)
Deploying
Finally we just need to deploy the app! Kamal always runs rails db:prepare
which will create
our SQLite database the first time and that should be it! Solid Cable in production
Verifying
To double check our SQLite instance is up and running and brokering our web socket messages as we expect, we can jump in to the console in production to see if we have any messages. Make sure to trigger an event in your app first and then check if the message was persisted:
kamal app exec -i 'bin/rails console'
> SolidCable::Message.last
#<SolidCable::Message:0x00007ff53fe4b8f8
id: 12,
channel: "item_106",
payload: "\"\\u003cturbo-stream action=\\\"prepend\\\" target=\\\"un...",
created_at: "2024-11-30 12:26:30.443258000 +0000",
channel_hash: -7306872458338484105>
Lovely! Here we can see a payload was captured to prepend some content on channel “item_106”.
Wrapping Up
OK! I didn’t find any articles specifically on actually deploying Solid Cable to production with SQLite while maintaining the primary database on PostgreSQL which prompted me to write this one. Future me thanks myself at least.
Don’t hesitate to drop me a note at my new favourite place Blue Sky or via any channels listed here