Deploying Ruby on Rails app to Heroku while using

2020-07-22 10:25发布

问题:

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.

回答1:

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