Note: This Rails version targeted in the post is one before webpacker got added. The folder structure and procedure might differ if you're using a newer version of Rails. But the context does help.
Deployment of a Rails app that uses Webpack to Heroku is tricky because it requires both Node.js and Ruby. After various unsuccessful attempts, I had a solution that worked without customizing Heroku buildpacks. If you know how Heroku buildpacks work, skip to the summary for the TL;DR version.
A little bit of background first. The application I'm working on uses React for writing interactive components on the front-end. Rails handles routing and database-related functions. The React components are written in ES6 syntax that gets transpiled to the more widely supported ES5 API via the Babel transpiler. ESLint is used for code linting, and Redux for handling complex state transitions of React components. All the modular JavaScript components are bundled into a single file using Webpack.
It's worth pointing out that there's already a well supported React gem
for use with Rails react-rails
, but we chose to use npm
instead. We wanted to leverage the latest version(s) of React, Redux,
ESLint, and we felt that using the npm-based package management workflow
works best in this case.
This is the application directory structure we decided to go with:
Initially the package.json
required by npm was placed inside the
webpack/
directory, but this made deployment to Heroku a bit
complicated. The Webpack asset compilation step bundles all the
JavaScript files into a single file called react_bundle.js
and places
it under the app/assets/javascripts
directory. This file is in turn
loaded into Rails' asset pipeline. So the prerequisite for the asset
pipeline to work properly is that a react_bundle.js
file must
be present in app/assets/javascripts
by the time assets:precompile
step is run during deployment.
Heroku buildpacks
Heroku uses setup scripts they call buildpacks
for
figuring out the type of application, and to run the necessary build
steps. For example, Heroku runs build steps for a Rack-based application
if it finds a Gemfile
in the root directory. If it finds a Railties
gem in the Gemfile specs, it will trigger the Rails related build steps.
At a simplified level, these are the steps that are run for a Rack
application:
- Check for
Gemfile
andGemfile.lock
in the root directory. If it's present, assume this is a Ruby application and start Ruby related build steps. - Check to see if specs in
Gemfile.lock
has Rack gem as a dependency. - Install necessary Ruby version
- Install bundler and install all dependencies
- Run a web process based on a Procfile or the default web process
In the same way, Heroku figures out the build steps for a Node.js
application based on the presence of a package.json
file in the root
directory. So what if we need both npm related steps and Ruby related
steps? We need to keep both the Gemfile
and package.json
in the root
directory. That's step 1.
Multiple Heroku Buildpacks
Heroku supports having multiple buildpacks for a
single application. If we use both Node.js and Ruby buildpacks, we need
a way to tell Heroku to run the npm-related steps first, and only then
to run the Ruby related steps. This is to ensure that the
react_bundle.js
file is present under app/assets/javascripts
when
Rails' asset pipeline commands are run. The Heroku command line provides
a way configure the build packs and specify an order to them:
The --index
argument means the heroku/ruby
buildpack is inserted
after the heroku/nodejs
buildpack. This completes step 2.
Post build hook on Heroku
Once the buildpacks are setup, we need a way to run a Webpack command
right after the Node.js setup is completed, and just before the
Ruby/Rails related setup starts. Heroku's Node.js buildpack provides a
way for users to run a custom script right after the normal
Node.js build steps are completed. For this, a command must be specified
in package.json
under the scripts
section with a heroku-postbuild
key. Here is the relevant section from package.json
:
This completes step 3. With these three steps, I was able to setup a workable Webpack + Rails scaffold on Heroku.
Summary
- Put the
package.json
in the root folder of the Rails application. - Setup multiple buildpacks on Heroku via Heroku's command line utility,
with
heroku/nodejs
in the first position andheroku/ruby
second. - Add the webpack bundling command in
package.json
'sscripts
section withheroku-postbuild
as the key.