ActionCable is WebSockets on rails. This lets you create realtime, interactive systems, where you can push data from one client to another client without reloading or polling. But how do we deploy it on heroku?
Let's go through the steps of how to deploy things on heroku.
First, the app
I'm going to use an app that dhh made as a walk through. What it does is
- creates a message model
- creates a rooms controller with a show action
- opens a websocket with the server
- that receives rendered messages on the
- and accepts messages to be broadcast to everyone else
- uses a background worker to render the message and broadcast it back to everyone
Now lets talk about deployment
The app we generated was done using basic rails 5 commands, and it's out of the box stuff. (Unlike most of the walkthroughs on this site which were done with seed. It's setup to use puma (good), which will handle the WebSockets. It's also setup with ActiveJob and ActionCable, both on which are in async local mode and won't use redis in development. ActionCable will also need to know where it's websocket is going to connect to. Lets walk through everything that needs to get done.
First we need to add some things to the Gemfile, specifically redis, postgres, and the sidekiq gems. Add the following to your
gem 'redis', '~> 3.0' gem 'pg', group: :production gem 'sqlite3', group: :development gem 'sidekiq' gem 'sinatra', git: 'https://github.com/sinatra/sinatra', :require => nil
Make sure you remove the
sqlite3 on the top of the file, we only want it in development. We are installing
sinatra out of the master branch since the current release doesn't play well with Rails 5. (If it tries to install Sinatra 1.0 you'll get rake exception errors.) If you don't want the web interface to sidekiq you can omit.
First we need to create a
Procfile that defines our dynos
web: bundle exec puma -C config/puma.rb worker: bundle exec sidekiq -c 2
I'm running sidekiq with a concurrency of 2 to limit the number of redis connections. You'll probably want that a lot higher in real life if you are using Sidekiq for other things..
config/initializers/active_job.rb and tell ActiveJob to use
Rails.application.config.active_job.queue_adapter = :sidekiq
And let's add the sidekiq web interace to your
require 'sidekiq/web' mount Sidekiq::Web => '/sidekiq'
If this was a real app, you'd limit who can get to that engine. If you installed devise, the way to do that is:
authenticate :admin_user do mount Sidekiq::Web => '/sidekiq' end
A brief digression to test locally
It's always nice to change as little as we can, so when everything blows up in our face we can limit our investigations to a small change set. So lets first migrate our development environment to communicate over redis, so the ActiveJobs are run in a seperate process, and that ActionCable will be able to communicate from that process, back to the main web server, back to the client.
config/cable.yml to set the development environment use to use redis.
development: adapter: redis url: redis://localhost:6379/1
If you don't have redis installed, use homebrew to do that now:
$ brew install redis $ redis-server
If you don't have foreman installed, do that now:
$ gem install foreman
Now lets run that app!
$ rails db:migrate $ foreman start
Using foreman will start both the web interace as well as the sidekiq worker.
Now you should be able to go to
http://localhost:3000 in multiple windows and talk with yourself. I really like talking with myself, and I assume that you do as well.
http://localhost:3000/admin should load up the sidekiq admin console. Validate that everything is working, since now we are going to spin up everything on heroku!
Heroku and Redis
Create a new app:
$ heroku create
Then we create a redis instance
$ heroku addons:create heroku-redis:hobby-dev
We now need to tell ActionCable where it's redis server is. Lets find out the answer and put in into the production section of
$ heroku config | grep REDIS REDIS_URL: redis://h:p75fl9as.........
production: adapter: redis url: redis://h:p75fl9as.........
Set the right ActionCable websocket address
We need to tell rails where it's expected to receive the websocket connection from.
heroku info will show us our external application. If you deploy on a custom url, you'll need to add that as well.
$ heroku info === aqueous-thicket-49913 Addons: heroku-postgresql:hobby-dev heroku-redis:hobby-dev Dynos: web: 1, worker: 1 Git URL: https://git.heroku.com/aqueous-thicket-49913.git Owner: firstname.lastname@example.org Region: us Repo Size: 34 KB Slug Size: 29 MB Stack: cedar-14 Web URL: https://aqueous-thicket-49913.herokuapp.com/
Lets tell rails about it. Edit
config/environments/production.rb. Obviously update the urls for your application.
config.action_cable.url = 'wss://aqueous-thicket-49913.herokuapp.com/cable' config.action_cable.allowed_request_origins = [ 'https://aqueous-thicket-49913.herokuapp.com']
wss is WebSockets over over SSL. You should use that, for a number of reasons including but not limited to security. However, if you don't use SSL, use
app/views/layouts/application.html.erb and add this into the
<%= action_cable_meta_tag %>
Commit and push
First we add the code to repo
$ git add . $ git commit -a -m "Added heroku configuration"
Now we push to heroku itself:
$ git push heroku master
If that goes well, lets create the database tables and spin up the worker process
$ heroku run rake db:migrate $ heroku ps:scale worker=1
And now, lets run the app and look at the logs:
$ heroku open $ heroku open $ heroku log --tail
We ran open twice, so you could see what was there in two windows. Does it work? Do 3 windows work?
Do you like talking to yourself as much as I do?
But does it scale
Right now you have 1 web dyno, running puma with 5 threads, and 1 worker dyno running sidekiq with 2 concurrent worker. This should be at least 5 redis connections, up to 2 more depending upon how many jobs have gone through sidekiq. Lets look at the redis info:
$ heroku redis:cli ec2-54-243-230-243.compute-1.amazonaws.com:24949> info .... # Clients connected_clients:7 client_longest_output_list:0 client_biggest_input_buf:0 blocked_clients:2 ....
So we see that we have 7 active connections now. As you get more people listening to redis, those will go up. Adding more dynos will push the number of active connections up, so you need to be careful in sizing both your postgres install as well as your redis install.
Image credit Lee Roberts