Sinatra with JRuby on Heroku

This post is now deprecated in favour of Heroku’s build packs.


A couple of days ago Heroku announced support for Java on their servers. I took the opportunity to try and get a Sinatra app running on Heroku using the Java support to boostrap JRuby.

In Heroku’s blog post they mentioned that Matthew Rodley had already put a Rails app on Heroku by simply adding JRuby to pom.xml. Looking at what Matthew had done it didn’t seem to hard to get something working. First thing is to setup a simple Sinatra app.

require "rubygems"
require "bundler"

Bundler.require

require "sinatra"

get "/" do
  "Hello World"
end

Nothing complicated there.

There are a few potential gotchas when working with Heroku though. From what I can tell if there is a config.ru or Gemfile present in the root application directory, Heroku will ignore the pom.xml and start the application using Ruby. I like Matthew’s approach of renaming the Gemfile to Jemfile.

source "http://rubygems.org"

gem "sinatra"
gem "trinidad"

The process of running a Java app on Heroku goes like this:

  1. Heroku detects pom.xml and runs a mvn install. This should install any Java dependancies.
  2. Heroku reads Procfile and executes web command.

Normally, the command that is in the Procfile is a script that is generated by the maven-appassembler-plugin. This script looks up the location of the JRE and sets the appropriate Java classpath so dependancies can be resolved when launching a Java application. The main difference with JRuby is what this script calls to launch our Ruby app. Rather than java net.example.MyApp we want something like java org.jruby.Main -S trinidad -p 4567.

Copying what Matthew did, I copied his script and placed in the script folder. Then I placed the following line in my Procfile.

web: sh script/jruby -S trinidad -p $PORT

The script jruby is a slightly modified version of a startup script that get’s generated by the maven-appassembler-plugin. Heroku will execute the above command when trying to start our app.

Lastly in our pom.xml we need to tell maven to pull down both JRuby and the JRuby rake plugin. JRuby also needs the gems that are required by our app. Again Matthew’s pom.xml does all of this.

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>au.com.mathew</groupId>
    <artifactId>jruby-heroku</artifactId>
    <version>1.0</version>
    <name>webapp</name>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.jruby</groupId>
            <artifactId>jruby-complete</artifactId>
            <version>1.6.3</version>
        </dependency>
        <dependency>
            <groupId>org.jruby.plugins</groupId>
            <artifactId>jruby-rake-plugin</artifactId>
            <version>1.6.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.jruby.plugins</groupId>
                <artifactId>jruby-rake-plugin</artifactId>
                <version>1.6.3</version>
                <executions>
                    <execution>
                        <id>install-bundler</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>jruby</goal>
                        </goals>
                        <configuration>
                            <args>-S gem install bundler --no-ri --no-rdoc --install-dir .gems</args>
                        </configuration>
                    </execution>
                    <execution>
                        <id>bundle-install</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>jruby</goal>
                        </goals>
                        <configuration>
                            <args>
                                -e ENV['GEM_HOME']=File.join(Dir.pwd,'.gems');ENV['GEM_PATH']=File.join(Dir.pwd,'.gems');ENV['BUNDLE_GEMFILE']=File.join(Dir.pwd,'Jemfile');require'rubygems';require'bundler';require'bundler/cli';cli=Bundler::CLI.new;cli.install
                            </args>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

The important part to note here is the in the plugins section. We’re telling maven to execute a couple of commands so that the required gems get installed into JRuby. First it executes:

jruby -S gem install bundler --no-ri --no-rdoc --install-dir .gems

This installs bundler. Then the following is executed to install the required gems.

jruby -e ENV['GEM_HOME']=File.join(Dir.pwd,'.gems'); \
   ENV['GEM_PATH']=File.join(Dir.pwd,'.gems'); \
   ENV['BUNDLE_GEMFILE']=File.join(Dir.pwd,'Jemfile'); \
    require'rubygems'; \
    require'bundler'; \
    require'bundler/cli'; \
    cli=Bundler::CLI.new; \
    cli.install

Note that Gemfile is specified as the new name Jemfile.

To summarise, this is what occurs when a push is sent to Heroku:

  1. Heroku detects pom.xml and runs mvn install.
  2. maven reads pom.xml and does the following:
    • Gets JRuby
    • Installs the bundler gem using JRuby
    • Installs the required gems to .gems using JRuby.
  3. Heroku inspects the Procfile and calls the web process that is defined.
  4. script/jruby -S trinidad -p <port> is called.

There was one problem that occured with the jruby script though. I was getting class not found errors from Java and the application would fail to load. By default it seems the REPO environment variable is undefined. The script default value to set if REPO is unavailable is $BASEDIR/repo, but this evaluates to app/repo, which doesn’t exist. maven installs dependancies to .m2/repository so I changed it to the following:

if [ -z "$REPO" ]
then
    REPO="$BASEDIR"/.m2/repository
fi

I’ve put this test project on GitHub for those interested.