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
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
source "http://rubygems.org" gem "sinatra" gem "trinidad"
The process of running a Java app on Heroku goes like this:
- Heroku detects
pom.xmland runs a
mvn install. This should install any Java dependancies.
- Heroku reads
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
web: sh script/jruby -S trinidad -p $PORT
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
To summarise, this is what occurs when a push is sent to Heroku:
- Heroku detects
pom.xmland does the following:
- Gets JRuby
- Installs the bundler gem using JRuby
- Installs the required gems to
- Heroku inspects the
Procfileand calls the
webprocess that is defined.
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.