Advice on using Ruby, RVM, Passenger, Rails, Bundler… in development
This article is the english version of “Conseils pour utiliser Ruby, RVM, Passenger, Rails, Bundler… en développement”, originally written in French.
If you have a better translation, I’d be happy to use it (full or parts) and credit you.
Last updated on March 3rd, 2011 : Rails 3 and Bundler are stable, versions of Passenger and REE are up-to-date.
Introduction : Why this advice
The dev team I’m working in is composed of 3 people who use Ruby.
Besides me, who’s is spending the most part of my time on Ruby and became “the one who knows” about this stuff, my co-workers don’t use Ruby the whole day, and use some others languages/frameworks (mostly Objective-C/iPhone and PHP/Symfony).
I’ve chosen to use some quite “bleeding edge” technologies for some reasons (I hope good ones) about performance, features, comfort… but their beta nature is not very easy to deal with on a daily basis.
It’s especially the case when there are some bugs, frequent changes in APIs, (temporary) incompatibilities… and when you don’t want to learn and understand every detail at every change. Sometimes, you just need it to work to “get things done”.
I’ll introduce briefly those tools and how they work together with the minimum effort and pain and how to get the most of them.
I emphasize the fact that I’m describing here a development context. I’ve not yet used RVM in production, even if I know that it seems to work really well.
The (not so) usual suspects
Ruby
Wait, What ? Ruby is not stable yet ?
Of course it is, but it’s also in a transition from 1.8 to 1.9 (1.9.2 released a few months ago). And there are many implementations (JRuby, Rubinius…). It is very useful to be able to test your code in those different implementations, with RVM.
RVM - Ruby Version Manager
The main idea behind RVM is to have several different rubies on the same system, in a separate environment from the default ruby installed by the OS vendor, and being able to switch very easily between them.
For each Ruby, it allows to have different groups of gems (Gemsets) to avoid version conflicts between projects.
RVM is rich and powerful, but it introduces some concepts and tools that are not so simple to use in the beginning.
If you’re not completely in phase with it, you can easily use the wrong Ruby and/or Gemset and quickly make quite a mess.
Happily, the documentation is really great and quite up-to-date, and the dev team (@wayneeseguin and his mentee @Sutto) is awesome and always available on IRC.
Ruby on Rails
I won’t describe Rails in details, it’s a well known web development framework written in Ruby.
Since version 3, some parts have stopped being compatible with the previous stable version (2.3), mostly about the “rails” command and the scripts installed in applications. It’ become difficult to use Rails 2 and Rails 3 on the same computer without RVM.
Rails 3 also brings a brand new dependency management tool called Bundler, as a required gem.
Bundler
Bundler is a gem, written in Ruby whose purpose is to manage dependencies in a Ruby project, for example a Rails application.
Much better than the “old” way when dealing with the dependency tree, it is especially excellent to quickly install all the gems required by a project in a restricted area, without interfering with others projects and vice versa.
Its stable version is 1.0 (1.0.10 to be accurate).
Phusion Passenger
Phusion Passenger connects Rack or Rails apps with Apache (or Nginx), allowing to host this kind of Ruby apps almost as easily as PHP scripts/apps.
It is now a stable piece of software, but using it with RVM and Bundler is not always obvious, especially if you want to use a different Gemset (and the gems inside it) for each hosted application.
Issues we had
Those past few months, we mostly had issues with following properly versions changes and dealing with incompatibilities. Although I managed to keep the pace, my co-workers had better things to do than spending hours each week to make all this stuff work. Some changes that I was committing in our repositories were leading to non-working projects on their side or at least some headaches for them. Some hair were pulled, and occasionally, all these beautiful tools were called with improper names ;-)
Recommendations
I’m going to describe the setup from a developer point of view (no production right now), with Mac OS X. It should probably be quite the same with Linux, even if some tools may not come pre-installed, or with different versions… For Windows, it’s a whole different thing. I have no viable information about what is working or not and this part of the universe is quite hostile with web development (especially Ruby).
Apple has been shipping Mac OS X with Ruby since Leopard (at least). This pre-installed version is not useful here, but it’s good to know that it exists, at least to differentiate from other rubies installed with RVM. Right now, on Mac OS X 10.6.4, Ruby 1.8.7-p174 is installed.
If you start from a state where you’ve never done anything regarding Ruby, gems… I advise not to instal any gem at all. Everything will be dealt with RVM.
Install RVM
The installation process for RVM is quite simple et well documented. You’d rather read it there than copy/paste from here.
Once RVM is installed, you can install other “rubies”. I suggest using Ruby Enterprise Edition instead of a regular 1.8.7 :
$ rvm install ree
I also suggest to make it the default RVM Ruby and the preferred on for Passenger.
$ rvm use ree --default --passenger
From now on, every new shell will use this version of Ruby and a wrapper script will allow Passenger to use it.
To update RVM itself you just have to
$ rvm get latest
If you want to use the latest from the Git repository instead of the latest release :
$ rvm get head
How to use RVM Gemsets
The Gemset concept
As a reminder, a Gemset is a hermetic environment, dedicated to the installation and use of gems.
Each Ruby installed with RVM has at least 2 Gemsets : default
(unnamed in fact), and global
. By default, we are in the unnamed Gemset. The global
Gemset makes every gem installed in it available for all other Gemsets of the current Ruby. Then you can create as many Gemsets as you like, for example 1 for each development project.
Here is an attempt to represent this visually :
/--------------------------|------------------|------------------\
| Ruby EE | Ruby 1.9.2 | other Ruby |
|--------------------------|------------------|------------------|
| @global | @global | @global |
|--------------------------|------------------|------------------|
| @default | @app1 | @appX | @default | @appX | @default | @appX |
\--------------------------|------------------|------------------/
A Gemset by project
For every web sites/apps I’m working on, I create a Gemset. The gems the projects can “see” are isolated from others, it’s like a specific environment for the project. For those managed with Bundler, the interest is slightly smaller, but for others (Rails 2, other frameworks or no framework at all) the interest is considerable.
One of RVM’s features is to recognize the presence of a .rvmrc
file in a directory when you enter it (with cd
for example). If this file refers to a Ruby and/or a Gemset, RVM makes the switch automatically. Each one of my projects has its .rvmrc
file containing at least this line :
$ rvm --create use default@projectX > /dev/null
In short, it will use the default Ruby (defined earlier, but changeable any time) and the Gemset “projectX” if it exists. If not, it is created right away. Redirecting to /dev/null hides the RVM’s output, otherwise it’s annoying to see it each time you enter such a directory.
Since version 1.0 (released on august 22nd, exactly 1 year after the first commit), each time RVM will find a new .rvmrc
file, it will ask if you trust it or not. It’s a security feature.
Gems in the global
Gemset
I always install some gems in the global
Gemset to have them available, whatever project Gemset I’m in (or if I’m outside any specific project) including the default
one.
- passenger
- capistrano + capistrano-ext
- bundler
- git_remote_branch
- awesome_print
- g
- wirble
Those gems will be available in any Gemset inside the Ruby associated with this global
Gemset.
Make RVM and Phusion Passenger a happy couple
Usually, Passenger uses a defined ruby binary and the “normal” environment variables to look for gems required by hosted applications.
When you install Passenger, you have to configure the webserver (here Apache, but there is a similar configuration for Nginx) with (at least) those 3 directives :
LoadModule passenger\_module /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4/ext/apache2/mod\_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4
PassengerRuby /usr/bin/ruby1.8
The key point is to tell Passenger to use the right Ruby, where the modules are… Here is my setup (adapted from RVM documentation) :
LoadModule passenger\_module /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4/ext/apache2/mod\_passenger.so
PassengerRoot /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4
PassengerRuby /Users/jlecour/.rvm/wrappers/ree-1.8.7-2011.03/ruby
With this, Passenger will use the module from the passenger
gem, installed in the global
Gemset of the default Ruby and the defined wrapper as a Ruby binary.
OK, but how Passenger will be able to use the right Gemset depending on the application ?
With Passenger >= 2.2.14 and RVM >= 0.1.42 it is possible to use (in Ruby) an API for RVM and adjust the environment variables for the hosted application. It will then know where to look for gems. It’s explained in details by Darcy Laycock (Sutto) in his blog post The Path to Better RVM & Passenger Integration.
Currently, Passenger has a limitation to use only 1 Ruby interpreter. Check out the documentation for more info.
For a Rails 3 application, with Bundler, here is my config/setup_load_paths.rb
file :
if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm')
begin
rvm_path = File.dirname(File.dirname(ENV\['MY_RUBY_HOME'\]))
rvm_lib_path = File.join(rvm_path, 'lib')
$LOAD_PATH.unshift rvm_lib_path
require 'rvm'
RVM.use_from_path\! File.dirname(File.dirname(__FILE__))
rescue LoadError
# RVM is unavailable at this point.
raise "RVM ruby lib is currently unavailable."
end
end
ENV['BUNDLE\_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'
For a Rails 2 application without Bundler, you just have to delete the last 2 lines.
With this, I can host several Rails 2 and Rails 3 application with Passenger, on the same webserver, without any version conflicts.
Use Bundler for dependencies management
I summarize once again ; Bundler allow to specify which gems (and their versions) required by a project, to install them (with dependency management) and load them exclusively in the project. The result is : easy installation, confidence in the used versions and no conflicts.
If you’ve been using Bundler <= 0.9.6, I strongly recommend to uninstall the previous versions, (unless you explicitly need them), install the pre-release version, then make Bundler re-install the required gems.
$ gem uninstall bundler
$ gem install bundler
$ bundle install
When you bundle install
, the Gemfile
is processed to build the dependency tree and a Gemfile.lock
is created. It is used to know exactly which version of what gems has to be installed.
As long as this file is not modified, the project will only see those gems. It guaranties that the development environment (including other developers) will use the same gems as those for tests, staging, production…
A developer who wants to update a gem or install a new one has to modify the Gemfile
file accordingly then do a bundle install
from the root directory of the project (where the Gemfile
file is). Then he can commit the changes in the repository for the other developers.
A developer who doesn’t want to manage gem version changes just has to let the Gemfile
and Gemfile.lock
files like they are. To verify he has every gem the project needs (in the right version), after an update of the codebase, or if the application doesn’t work, he just has to do a bundle install
from the root directory of the project. This way, he’s sure to be up-to-date.
Using RVM make all this even easier, faster and reliable. In normal
mode, Bundler installs the gems system wide, with all the other gems, but since what he sees as the whole system is only a Gemset, everyone is stays in its yard.
In deployment
mode, it installs gems in a cache directory, inside the application. It looks like a Gemset. It is possible to optimize this step with Capistrano
Bundler and Capistrano for an easy depoyment
An application managed with Bundler doesn’t use the usual require methods to load the gems. Bundler takes care of this when the application is initializing. To do this, the gems have to be accessible to Bundler on the application server. We have to check if everything is OK after each deployment and if possible share the gems between releases of the application to avoid unnecessary downloads and disk usage.
Since version 1.0.0.rc.5, Bundler has a Capistrano task (in fact since rc.4 but it had some bugs). You just have to add the line require 'bundler/capistrano'
in your deployment recipe (deploy.rb
). You can tweak some parameters, but by default, the gems are going into shared/bundle
.
Other major packages
For the management of additional packages that Apple does not provide, I usually suggest to use Homebrew. Contrary to MacPorts, it doesn’t install every library or package systematically. If it finds what it needs in the Apple provided libraries (like OpenSSL…) it’s going to use them instead of downloading/compiling/installing them. It’s faster, more coherent and lighter.
Homebrew also install everything it needs with the user’s permissions, without sudo
. It seems a little disturbing for a sysadmin, but in a development context, it makes a lot of sense.
I’ve used Homebrew to install major packages as MySQL
, Git
, ImageMagick
, MongoDB
… and numerous small tools like tree
, wget
, ack
…
Credits
First, I’d like to thank all the developers and contributors of those awesome tools.
A special thank you for Wayne E Seguin and John Mettraux for the meticulous typo hunting, Thibaut Barrère and Sébastien Gruhier for their support and additional tips.