Over the weekend, I was rethinking the way we should deploy our Merb application (our API) for Defensio. Everything I’ll say here applies to Rails as well.
Managing gems has been painful lately. Like most people, we’re freezing them, which helps tremendously. However, there’s a few problems with this approach:
- We can’t freeze C extensions because they need to be compiled for the target platform. We develop on OS X and Linux 64, but deploy to Linux i386. As a workaround, we currently freeze but recompile them on every deployment. Definitely not optimal.
- Our frozen gems directory is getting huge.
- It’s becoming complicated and quite a mess to manage multiple gem versions for different branches/tags.
- We never quite know which branch needs which gem.
Of course, we could manually install the gems we need on our servers. But the word “manually” generally leads to “epic fail”.
So I thought, why not install them at deployment time, just before restarting the Mongrels, with Capistrano? That seemed like a pretty good idea! The only problem is that Rubygems will always install a gem when asked to do so, even if it’s already installed.
That’s where install_gems.rb comes in. install_gems.rb takes a file name as an argument. This file contains a simple list of gems along with their version number. A custom installation command can also be specified.
Here’s an example:
# This is an example file. As you can see, it supports comments.
haml 2.0.3
eventmachine 0.12.2
# Notice the custom command specified after the --
termios 0.9.4 -- gem install termios -v 0.9.4 --no-rdoc --no-ri
To install the above gems, you’d simply run
./install_gems.rb input_file
install_gems.rb has a few advantages over vanilla rubygems:
- It will only install a gem if the specified version is not already installed. That’s obviously much faster than reinstalling everything every time.
- C extensions will be properly compiled.
- Using different gem versions in different branches/tags/releases is now very easy. You just have to maintain a list of required gems in your Git repository and make sure Capistrano runs install_gems.rb against this list before restarting the application servers.
- It forces us to keep an up-to-date list of the gems we need.
Of course, for this to work as expected, you’ll need to require specific gem versions in your application. Your production server will likely have multiple versions of the same gems installed and if you don’t specify which version of the gem you want to require, things might (or will) break.
I released install_gems.rb on GitHub and Daniel Haran has already submitted a patch. Feel free to do so as well, I’m a puller!
13/10/2008 at 7:49 pm Permalink
Hey, just wondering if you took a look at geminstaller: http://geminstaller.rubyforge.org. Seems like it performs most of the tasks you described.
13/10/2008 at 7:54 pm Permalink
I wasn’t aware of geminstaller. Thanks for the tip! I think I still prefer the simplicity of my script though. It does one thing and does it well
22/01/2009 at 7:30 am Permalink
To avoid redeploying all your gems every time you deploy, add a new task in your cap recipe to create a symlink between your shared folder /gems/gems/ and your current app gems/gems.
The redeployment task (thor merb:gem:redeploy) will only take care of the newly bundled gems and you not rebuild already available gems.
I’ll release my capistrano’s recipe in the next few days to show what I mean.
- Matt
23/01/2009 at 9:12 pm Permalink
We basically used this solution before the Merb bundler was available. We’ll eventually move to the built in solution I guess, but this solution is still pretty decent imo. Hasn’t failed on us yet!