Rails等で作ったちょっとしたWebアプリケーションを動かす時に、
Herokuにデプロイするとお手軽で便利ですが。
近頃はちょっとしたアプリケーションでも、
バックエンド(API)と、
フロントエンドのServerSideRenderingなどを組み合わせることも多くなってきました。
小さなアプリケーションを試す場合に、
バックエンドとフロントエンドで個々にデプロイするのは面倒だと思います。

そこで、このエントリでは、
バックエンドとフロントエンドが異なる言語で構成されたアプリケーションを
1つのdynoで動かす手順について記載します。

実現方法の整理

まず、Multiple Buildpacksを使うと1つのdynoで異なる言語を動かすことが可能になります。

Using Multiple Buildpacks for an App | Heroku Dev Center
https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app

次に、以下の方法を用いると1つのdynoで複数のプロセスを実行することが出来ます。

How do I run multiple processes on a dyno? | Heroku Help
https://help.heroku.com/CTFS2TJK/how-do-i-run-multiple-processes-on-a-dyno

これらを組み合わせると、
バックエンドとフロントエンドが異なる言語で構成されたアプリケーションを
1つのdynoで動かせることがわかります。

環境を作る① - Railsアプリ(API)の作成

それでは実際に環境を作っていきます。
まずはバックエンド部分としてRailsでAPIを作成します。

$ rails new multi-language-single-dyno-sample --api --skip-active-record
$ cd multi-language-single-dyno-sample

ここでは、 以下のように現在時刻を返却するAPIを実装します。

app/controllers/sample_controller.rb

class SampleController < ApplicationController
  def current_time
    render json: { 'current_time': Time.now() }
  end
end

config/routes.rb

Rails.application.routes.draw do
  get '/current_time', to: 'sample#current_time'
end

サーバを起動して動作を確認します。

$ bundle exec rails s

http://localhost:3000/current_time にアクセスして、
現在時刻がJSONで返却されればOKです。

環境を作る② - Express(Node)アプリの作成

次にフロントエンド部分としてExpress(Node)のアプリを作成します。

Railsアプリのディレクトリと同じ場所で、
npmを初期化して、express、 express-http-proxyを追加します。
express-http-proxyは、APIをプロキシするために使います。

$ npm init
$ npm add express
$ npm add ejs
$ npm add express-http-proxy

expressを起動するindex.jsを用意します。

index.js

const proxy = require('express-http-proxy');
const express = require('express')
const path = require('path')
const PORT = process.env.PORT || 5000

express()
  .use(express.static(path.join(__dirname, 'public')))
  .set('views', path.join(__dirname, 'views'))
  .set('view engine', 'ejs')
  .get('/', (req, res) => res.render('pages/index'))
  .use('/api', proxy('127.0.0.1:3000'))
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))

出力するHTMLファイルを作ります。

$ mkdir -p views/pages

views/pages/index.ejs

<html>
<head>
 <title>hello express</title>
</head>
<body>
<script>
var request = new XMLHttpRequest();
var hostname = location.host;
request.open('GET', '//' + hostname + '/api/current_time', true);
request.onload = function () {
  var data = eval("(" + this.response + ")");
  document.write(data['current_time']);
}
request.send();
</script>
</body>
</html>

Herokuで実行出来るようにProcfileを用意します。

Procfile

web: node index.js

Heroku localで動かしてみます。

$ heroku local

以上で、http://localhost:5000/ にアクセスすると、
APIから現在時刻を取得して表示します。

環境を作る③ - Rails/ExpressをHeroku Localで実行する

一旦、RailsとExpressを終了して、
Procfileで両方あわせて起動できるように設定します。

Rails(puma)とexpressを別ポートで起動するように、
pumaの設定で参照するPORTの環境変数を変更しておきます。

config/puma.rb

修正前: port        ENV.fetch("PORT") { 3000 }
修正後: port        ENV.fetch("PORT_API") { 3000 }

両方あわせて起動できるように、Procfileを変更します。

Procfile

web: PORT_API=3000 puma -C config/puma.rb & node index.js & wait -n

Heroku localで動かしてみます。

$ heroku local

http://localhost:5000/ へのアクセスで同じ結果が表示されます。

環境を作る④ - Herokuにデプロイ

以上でローカルで動く環境が構築出来たので、Herokuにデプロイしてみます。

Gitにcommitします。

$ echo "node_modules/*" >> .gitignore
$ git add .
$ git commit -a -m initial-commit

Herokuにアプリを作り、ruby, nodejsのbuildpackを追加します。
その後、モジュールをpushするとHerokuにデプロイできます。

$ heroku create --buildpack heroku/ruby
$ heroku buildpacks:add --index 1 heroku/nodejs
$ git push heroku master

表示されたURLにアクセスするとローカルと同じ結果が表示されるはずです。

以上の流れで、
2つの異なる言語で構成されたアプリケーションを1つのdynoで動かす事が出来ました。
この手順を、APIとフロントが分かれている小さなアプリを実験する時などに、
役立てて頂ければと思います。