In a series of posts from a few months back (ActionCable Part I, Action Cable Part II), I explored Rails' ActionCable and built out an app in which users can post code snippets in chat room-like forums in real-time.
In those posts, we saw ActionCable running on a separate, standalone server. As much fun as it was to deploy that app (read: not fun), ActionCable has since been merged into Rails and can now be run alongside your main Rails app, on a single server.
Given this development (and with the encouragement of a few folks out there on the internet), I decided to re-work and re-deploy my earlier application using a single server. I found it to be a pretty seamless transition and the deployment was roughly 100% less painful than previously.
The App
What this app actually does doesn't matter! If you want to learn more about how to implement ActionCable to build out real-time updates, like messaging between users, check out this earlier post. This post is about configuring and deploying ActionCable in Rails on a single server. So, we're assuming we're working with an app that already has ActionCable built out on the client side and on the server side. Here, we'll configure our server to run the cable server in conjunction with our main app, and we'll configure Redis.
Let's get started.
Configuring Action Cable in Development
Configure Puma
ActionCable can be run together with your Rails app, on a single server, with any threaded server. For this app, I chose Puma, although ActionCable can be implemented just as well with Unicorn or Passenger. Check out this article from Go Rails to see ActionCable used with Passenger.
Add gem 'puma'
to your Gemfile and bundle install. Then, set up Puma in config/puma.rb
:
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['MAX_THREADS'] || 5)
threads threads_count, threads_count
preload_app!
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
on_worker_boot do
# Worker specific setup for Rails 4.1+
# See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
ActiveRecord::Base.establish_connection
end
Configure Redis
Create a file, config/redis/cable.yml
, in which to specify the Redis URL for each environment:
local: &local
:url: redis://localhost:6379
:host: localhost
:port: 6379
:timeout: 1
:inline: true
development: *local
test: *local
Create a file, config/initializers/redis.rb
:
uri = URI.parse(ENV["REDISTOGO"] || "redis://localhost:6379/" )
REDIS = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
Configure the Cable Server
In your routes file, add the following:
mount ActionCable.server => '/cable'
This will allow you to establish a Web Socket connection, listening for requests on /cable
.
Next, we set the server URL in config/development.rb
:
config.action_cable.url = "ws://localhost:3000/cable"
Lastly, configure the client. In this case of this particular app, the Lab model acts as the conversation or chat room. So, our consumer code belongs in app/assets/javascripts/channels/labs.js.erb
:
//= require cable
//= require_self
//= require_tree .
this.App = {};
App.cable = Cable.createConsumer();
Lastly, add the following to your app/views/layouts/application.html.erb
in the head:
<%= action_cable_meta_tag %>
By using this tag, we can create our consumer without specifying the server URL as the argument to the createConsumer
function. Instead, we specify the URL in the development and production environment files. This allows us to easily manage our cable connection in both development and production without having to constantly switch back and force between different server URL specifications in the createConsumer
function.
Now, a new instance of ActionCable will be created every time a new instance of our server is created. If you connect to your server with $ rails s Puma
, you should see your ActionCable feature working.
Depolying ActionCable to Heroku
First, run heroku create
in the directory of your app.
Configure Puma for Production
We need to set Puma as the server for our web processes. In the Procfile
:
web: bundle exec puma -p $PORT
Configure Redis for Production
Install the Redis To Go add-on:
$ heroku addons:create redistogo
Confirm your Redis To Go URL with:
$ heroku config --app <your fancy app name> | grep REDISTOGO_URL
You should see your URL returned below. It will look something like this:
REDISTOGO_URL => redis://redistogo:44ec00004dd4a5afe77a649acee7a8f3@thing.redistogo.com:9093/
Next, open up config/redis/cable.yml
and configure Redis for the production environment:
development: &development
:url: redis://localhost:6379
:host: localhost
:port: 6379
:timeout: 1
:inline: true
test: *development
production: &production
:url: redis://redistogo:44ec00004dd4a5afe77a649acee7a8f3@thing.redistogo.com:9093/
:host: thing.redistogo.com
:port: 11662
:password: 44ec00004dd4a5afe77a649acee7a8f3
:inline: true
:timeout: 1
Configure the Cable Server
We need to set the cable URL for our production environment:
# config/environments/production.rb
config.web_socket_server_url = "wss://your-amazing-app.herokuapp.com/cable"
Allow Request Origins
The Web Socket connection can only accept requests from allowed hosts. In development, this defaults to localhost:3000
. In production, we need to specify the URL of our app, i.e. the URL from which requests to the socket connection will be sent.
In app/config/environments.production.rb
Rails.application.config.action_cable.allowed_request_origins = ['<your-amazing-app>.herokuapp.com']
And that's it! We should be able to successfully git push heroku master
and have our ActionCable feature running in production. A few things before you go...
Gotchas
A few of the pain points I ran into:
- ActionCable and Spring do not play nice. If you're building your app from scratch, run
rails new --skip-spring
. Otherwise, comment out or remove Spring from your Gemfile. - If you want to use the Rails 5 pre-release, run
gem install rails --pre
before generating your new Rails app. If you are sticking with Rails 4, remember to include the ActionCable gem. In your Gemfile:
gem 'actioncable', github: 'rails/actioncable', branch: 'archive'
- I had an issue with the Celluloid gem, which is a dependency of the ActionCable gem, although this seems to have been removed when ActionCable was merged into Rails 5 beta. I had to specify version
0.16.0
to get it working. In my Gemfile:
gem 'celluloid', '~> 0.16.0'