Rails + Webpacker Deployments on Heroku

A few weeks ago I decided to take the leap and implement Webpacker into an existing Rails project. Initially this seemed like a great idea and almost too easy thanks to the rails/webpacker project. After a couple of hours I had everything working and some existing React components migrated from a custom compile system to the asset pipeline. Deploying to staging had a few minor hiccups, but nothing major and the project was ready for sign-off and production release.

For this specific project I'm using Heroku coupled with the pipelines workflow. For those not familiar, pipelines allow you to "promote" a slug almost instantly to production that is identical to staging. Everything worked as expected, assets were being served and development was now much simpler thanks to the Webpack-dev-server and access to tons of ES6 and JSX helpers out of the box.

A few days later I started seeing errors in Sentry from our frontend, complaining that libraries were unavailable and JavaScript files could not be downloaded from the CDN. After inspection it appeared production assets were being loaded from the staging CDN URL. In hindsight this was pretty obvious, but I want to share the findings so others can either avoid this or find a solution much faster.

Webpacker works based on the concept of a manifest, which writes absolute URLs to disk, which Rails then reads to figure out the pack file URL to render. With the asset pipeline, this manifest uses relative file paths and the config.action_controller.asset_host variable is used at run-time to build the asset URL. However, since Webpacker writes an absolute URL path at compile-time this is transferred to production via the pipeline promote workflow.

This wasn't initially obvious, but I really should have understood the Webpacker internals before pushing to production. This also didn't surface until a few days later because the assets were still on the staging CDN. As soon as new code was pushed to staging, and the fingerprinted hashes changed then the production URLs started receiving 404s once the file origin expiration came around.

This all happened in the middle of the night for us, as do all production issues, but the middle of the day for our users who are primarily in a different time zone. In the interest of getting this resolved as fast as possible I decided that bypassing the pipeline workflow all together and digging into the source code of Webpack after some sleep was the best approach. Without the pipeline workflow, pushing to Heroku production directly causes the whole build to run from the beginning which takes much longer, but means the absolute URLs written to disk will be the production URLs rather than staging.

Once this decision was made, I now needed to figure out how to force rebuild the current production servers. One approach is to push an empty commit (git commit --allow-empty -m "Nothing to see here") but I prefer to keep the git history clean. The code should not be coupled to deployments in this way in my opinion. Previously I'd used a plugin called heroku:repo which luckily still works, although I'd read in various places it was now deprecated. It turns out it was actually moved from it's original location to the Heroku Github organization, rather than removed.

Before being able to rebuild, I actually needed to reconfigure the project buildpacks to get the latest version of yarn, since builds were failing due to a new version of Webpack in the code. When migrating the initial code, Webpack worked fine with the ruby buildpack alone. Since then it now requires yarn >=v0.25, but the ruby buildpack only has v0.22. There is a pull request open to fix this, but until then the nodejs is the best approach. I also prefer using the nodejs buildpack going forward to protect against future dependency requirements of Webpack. The nodejs buildpack is also way more likely to have the latest version of yarn, including security patches. The most important thing to note is that the ruby buildpack should come after the nodejs buildpack so that the correct version of yarn is available.

heroku buildpacks:set heroku/nodejs -a yourapp
heroku buildpacks:set heroku/ruby --index 2 -a yourapp

Here are the commands to quickly rebuild the production servers. This can be used on any Heroku deployment, in any stage whether it uses pipelines or not.

heroku plugins:install heroku-repo
heroku buildpacks:clear -a yourapp
heroku buildpacks -a yourapp
heroku repo:reset -a yourapp
git push heroku-yourapp master

The moral of the story here is always be careful when mixing multiple language build processes that require compilation, and understand as much of the internals of the build process as possible. In this case better knowledge of Webpack manifests would have helped hugely. I'm sure there is a way to configure relative URLs in the manifest, but I'm yet to find it.