Notes from Sinatra, Heroku and MongoHQ deployment

June 29, 2010

For previous projects I never used Heroku, sometimes because I needed tools not available on the platform, other times because I had other options that were more interesting, price-wise.

My last side-project sounded like a perfect fit to trial Heroku though. My conclusion as you may have guessed is that Heroku is really addictive and sweet to use, I encourage you to give it a try.

Overall impression

The deployment was absolutely quick (less than 30 minutes apart from DNS config) and flawless. The site is quite snappy as well, and the uptime has been wonderful.

Technical overview

  • I used the new Bamboo stack which allows Ruby 1.9.1
  • the app itself is based on Sinatra with HAML and SASS
  • I struggled a bit to get the DNS to work but it went ok – maybe I should have tried using Zerigo as suggested
  • I used Bundler for the gems
  • the database started with Sqlite then we moved to MongoDB 1.4, hosted on MongoHQ (free plan)
  • the data aggregation system is built on TinyTL, ActiveWarehouse-ETL’s unreleased tiny brother
  • as I used a lot of data (html, xml, csv) caching locally, I used api-cache which works wonders
  • image resizing is done on my dev machine using ImageMagick via command line, then pushed via git

Exceptions notifications

For some reason, I could not get Exceptional to work with my Sinatra setup. Here’s what I used:

error do
  e = request.env['sinatra.error']
  msg = ... 
  send_email('Exception!', msg)
  haml :'500'
end

Graphical tips

I used Google Web Fonts, jQuery Corners, Compass and SASS to finetune the Blueprint CSS-based layout.

Notes on MongoDB

This site relies heavily on an ETL process in the background.

I’ve been playing a lot with ETL and MongoDB recently and I find that this database is very convenient to munge data coming from different sources and in different formats (html, xml, csv).

Being able to add any number and variety of columns on the fly as you integrate the data sources is a big win: MongoDB is very interesting for that matter.

MongoHQ offers a free plan to get started so I took the plunge. The internal AWS connection between Heroku and MongoHQ seems to work fast enough.

Data caching

Api-cache is worth a look to cache just about anything. Quick snippet:

require 'moneta/basic_file'

preprocess {
  folder = File.dirname(__FILE__) + '/cache'
  FileUtils.mkdir(folder) unless File.exists?(folder)
  APICache.store = Moneta::BasicFile.new(:path => folder)
}

def some_cached_operation(key)
  APICache.get(key,
    :cache => 60*60*24*10,
    :valid => 60*60*24*4,
    :period => 2) do
    process_some_heavy_download_with_polling_limits
  end
end

each_row { |row|
  row[:data] = some_cached_operation(row[:key])  
}

Heroku tips

Migrating stack to MRI 1.9.1

I started with the wrong stack but this can be easily changed:

$ heroku stack:migrate bamboo-mri-1.9.1

Database setup

By default the database uses a locally running MongoDB instance. I update the production database from the developer workstation by using env vars for setting up the connection:

if ENV['MONGOHQ_HOST']
  puts "Running on MongoHQ" 
  MongoMapper.connection = Mongo::Connection.new(
    ENV['MONGOHQ_HOST'], ENV['MONGOHQ_PORT'])
  MongoMapper.database = ENV['MONGOHQ_DATABASE']
  MongoMapper.database.authenticate(
    ENV['MONGOHQ_USER'], ENV['MONGOHQ_PASSWORD'])
else
  puts "Using local database" 
  MongoMapper.database = 'tout-pour-mon-ipad'
end

On traditional hosting you would put a config.yml path in the Capistrano shared folder, here you use vars that are managed by Heroku itself, and kept between deployments.

$ heroku config:add MONGO_HOST=flame.mongohq.com ....

These variables are then set in the application ENV.

Email setup

I use SMTP for the contact form and simple exception notifications. I used Pony and the free SendGrid add-on.

require 'pony'
SMTP_OPTIONS = {
  :address        => "smtp.sendgrid.net",
  :port           => "25",
  :authentication => :plain,
  :user_name      => ENV['SENDGRID_USERNAME'],
  :password       => ENV['SENDGRID_PASSWORD'],
  :domain         => ENV['SENDGRID_DOMAIN'],
}

def send_email(subject, html_body)
  Pony.mail(
    :to => '...', :from => '...',
    :subject => subject,
    :html_body => html_body,
    :via => :smtp,
    :via_options => SMTP_OPTIONS)
end

Conclusion

I’m very happy with the setup and overall experience so far, and it has been very nice to have no hosting cost at all to trial the idea.