missing runtime library path breaks execjs and coffescript in production setting

execjs cannot find the right JavaScript implementations unless runtime paths are set

When I deployed a Rails site the other day, I was greeted with a colorful yet stern error page from my web server (nginx/passenger):

passenger server error
Blam! Server Error, Rails App Refuses to Initialize

Some folks say adding a dependency to therubyracer -- a second server side JS implementation -- is an easy way to solve this, even though the application already uses the libv8 gem. However, this additional dependency can create other problems (like libv8 and therubyracer requiring mutually exclusive versions of v8 in their latest version -- ugh!), especially considering the machine dependent nature of the optimized implementations, which can really muck up testing and deployment across different types of systems.

Better to figure out what's broken. Maybe it's something simple, easy to fix.

Turns out it is: Some gems, especially ones calling none-ruby code via ffi, or running external processes altogether to do their bidding, need to have access to the right libs and binaries at runtime. If they don't have the right binary or lib paths, they just won't work, and they sometimes fail in annoyingly obscure ways. Here are some very common ones:

  • coffescript/execjs/libv8 - breaks with Passenger error pre-rails-startup page complaining "PhusionPassenger::UnknownError - Could not find a JavaScript runtime. See https://github.com/sstephenson/execjs for a list of available runtimes (ExecJS::RuntimeUnavailable)"
  • Pygments.rb - just refuses to highlight, raising an exception instead

Setting your runtime paths (PATH and LD_LIBRARY_PATH under FreeBSD to include /usr/local/lib, for instance), will fix that. You can do that at several level, I prefer to do it at the Rails config level by appending things to ENV['PATH']/ENV['LIBRARY_PATH']. Here is what I have in my application.rb:

# apply local envars and extend PATHs if necessary
env_yml = File.expand_path('../env.yml', __FILE__)
if File.exist? env_yml
  YAML.load(File.open(env_yml)).each do |key, value|
    name = key.to_s
    if name[-7..-1] == "_extras"
      path_name = name[0..-8]
      path = (ENV[path_name] || "").split(File::PATH_SEPARATOR)
      path_extras = value.split(File::PATH_SEPARATOR)
      path_extras.each do |extra_path|
        if File.exist?(extra_path) && ! path.include?( extra_path )
          path << extra_path
        end
      end
      ENV[path_name] = path.join(File::PATH_SEPARATOR)
    else
      ENV[key.to_s] = value
    end
  end
end

Make sure this goes almost at the top, after the require 'rails/all' but before the Bundler.require
Then, I have this in my local envar settings (config/env.yml):

DB_DEVELOPMENT_PASSWORD: ...
DB_PRODUCTION_PASSWORD: ...

GITHUB_API_ID: '...'
GITHUB_API_SECRET: '...'

LINKEDIN_API_ID: '...'
LINKEDIN_API_SECRET: '...'

PATH_extras: /usr/local/bin
LD_LIBRARY_PATH_extras: /usr/local/lib:/usr/lib:/lib

You could also adjust your Passenger or Nginx settings if you prefer, but the way I see it, this is a matter between bundler and what my app's required gems need on this local machine, much like the other app specific config.