I have got Action Cable working in a local host environment and in this situation I start the Puma server using a simple file containing
# /bin/bash
bundle exec puma -p 28080 cable/config.ru
Once this happens the puma server starts and is listening to this 28080 port and the port the local server is running on. Through hunting online I couldn't find a place that would tell me a way to emulate this on heroku or a way to have my server always start on the same port (though I don't know if that would give me the desired result either)
I have a javascript file set up to create a consumer related to that port.
//= require cable
//= require_self
//= require_tree .
this.App = {};
App.cable = Cable.createConsumer('ws://127.0.0.1:28080');
I imagine I'll need to change the 127.0.0.1 part as well for the deploy to heroku to work as well but I'm not certain. I attempted to cut off the 28080 part and replace it with ENV['PORT'] but it said it was an unknown variable even though I have a puma.rb file set up which has its port set as
... (only part of the file)
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
...
So it seemed to me that ENV['PORT'] was being defined as when I check the heroku logs the puma server will be
2015-07-26T06:50:25.278030+00:00 heroku[web.1]: Starting process with command `bin/rails server -p 48875 -e production`
2015-07-26T06:50:30.760680+00:00 app[web.1]: => Booting Puma
2015-07-26T06:50:30.760714+00:00 app[web.1]: => Rails 4.2.1 application starting in production on http://0.0.0.0:48875
2015-07-26T06:50:30.760716+00:00 app[web.1]: => Run `rails server -h` for more startup options
2015-07-26T06:50:30.760718+00:00 app[web.1]: => Ctrl-C to shutdown server
2015-07-26T06:50:31.578843+00:00 app[web.1]: Puma 2.12.2 starting...
2015-07-26T06:50:31.578851+00:00 app[web.1]: * Min threads: 0, max threads: 16
2015-07-26T06:50:31.578859+00:00 app[web.1]: * Environment: production
2015-07-26T06:50:31.578861+00:00 app[web.1]: * Listening on tcp://0.0.0.0:48875
I apologize if anything is unclear and would be happy to provide any more information if I left anything out.
EDIT
Here is the updated code in /app/assets/javascripts/channels/index.js.erb
//= require cable
//= require_self
//= require_tree .
this.App = {};
App.cable = Cable.createConsumer('<%= ENV["CABLE_SERVER"] %>');
where ENV["CABLE_SERVER"] points to ws://the-action-cable-server.herokuapp.com
. This variable is stored in the rails server env variables.
Issue
There are some limits to the Heroku router: it will only listen to ports 80 and 443. In other words, you can't open a fixed port on any Heroku application. In the ActionCable server case, there is no way to open a fixed port and get the websocket traffic routed to it. So either Heroku allows such things (which I doubt) or we use a workaround.
Workaround
As of version 0.0.3 of actioncable, here is the workaround I used.
The idea is to have not one Heroku application but two: one for the main rails server and one for the ActionCable server. Both would run on port 80 (or 443).
To run two different servers from one codebase is simply a matter of having two Procfiles: one for rails and one for action cable. There is a buildpack to handle this. To use it, you also need the multi buildpack.
Let's say you have your two Heroku applications called rails
and actioncable
.
Create a .buildpacks
file in the root of your project with this:
https://github.com/cantino/heroku-selectable-procfile
https://github.com/heroku/heroku-buildpack-ruby
On rails
and actioncable
, create an env var BUILDPACK_URL
with https://github.com/heroku/heroku-buildpack-multi.git
Now for the Procfiles, I choose to keep Procfile
for running everything locally with foreman and create two custom ones: Procfile.rails
and Procfile.actioncable
.
In Procfile.rails
, you describe all the needed dynos except the action cable server, eg.:
web: bundle exec puma -C config/puma/config.rb
clockwork: bundle exec clockwork lib/clockwork.rb
worker: bundle exec rake jobs:work
In Procfile.actioncable
, you describe only the action cable server:
web: bundle exec puma -p $PORT cable/config.ru
Note that we are using a web
dyno which will mount the action cable server on port 80 (or 443).
CAUTION You need to move the puma config file config/puma.rb
to a custom location. I choose config/puma/config.rb
. config/puma.rb
is the default location when you start puma without any specific config file, which is what we have in Procfile.actioncable
. This can lead to unexpected behaviors (see the comments below).
Now, on rails
, create an env var PROCFILE_PATH
with Procfile.rails
and on actioncable
, create an env var PROCFILE_PATH
with Procfile.actioncable
.
Speaking of env vars, the actioncable
needs all the env vars from rails
that are necessary to start the actioncable server like the DATABASE_URL or any credentials.
Now the crucial step: how do we connect rails
and actioncable
together? This is simply done by using the same Redis instance. rails
will put messages on Redis and actioncable
will listen to them and act accordingly. That's why both have to target the same Redis instance. If you using Heroku Redis, you just need to set REDIS_URL
with the same value on rails
and actioncable
. Here is the config file for the cable server cable.yml
:
production: &production
:url: <%= ENV["REDIS_URL"] %>
:timeout: 1
development: &development
:url: <%= ENV["REDIS_URL"] %>
:timeout: 1
:inline: true
test: *development
The final step is changing the javascript file so that we can control where is the Action Cable server. We can do this by using an env var.
Change the .js
suffix into .js.erb
if necessary.
//= require cable
//= require_self
//= require_tree .
this.App = {};
App.cable = Cable.createConsumer('<%= ENV["CABLE_SERVER"] %>');
This CABLE_SERVER
variable can now point to ws://127.0.0.1:28080
locally and on rails
, the value will be the url of actioncable
.
Now you're ready to deploy the code on rails
and actioncable
.
Caveats/Drawbacks
- on
actioncable
, if you have any client authentication, you can't use cookies
like in the examples. You have now two Heroku apps and they can't share a cookie. I guess you can still workaround this using a cookie for multiple subdomains.
- you have now two deploy targets to maintain.
- you have multiple Procfiles to maintain.
- hopefully, with time, there will be an easier workaround :)
Alternatives
- We can have the same server handling the normal web traffic and the websocket traffic using a simple middleware. Here is how.
- As of rails 5 beta2, the action cable server can now also be side-mounted along with your main rails app. Having two different servers can still make sense: you can scale them individually.
hth