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.<<: *defaultis a YAML merge key that pulls in the configuration from the&defaultanchorhost,database,usernameandpasswordobviously are the location, name and auth credentials for your database. Best practice is to pass these in from environment variables as showncableis 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<<: *sqliteis another YAML merge key that pulls in the config from the&sqliteanchordatabasepay 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_pathsis 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