Sinatra with JRuby on Heroku
This post is extremely old and contains outdated information. It is being kept only for historical purposes.
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:
- Heroku detects
pom.xml
and runs amvn install
. This should install any Java dependancies. - Heroku reads
Procfile
and executesweb
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:
- Heroku detects
pom.xml
and runsmvn install
. maven
readspom.xml
and does the following:- Gets JRuby
- Installs the bundler gem using JRuby
- Installs the required gems to
.gems
using JRuby.
- Heroku inspects the
Procfile
and calls theweb
process 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.