Building a Rails Development OpenSolaris Container
A bit over a year ago I started working with Ruby on Rails and ActiveScaffold for an in-house scheduling application I was building for a new consulting client. At the time the version of Rails was 1.2.3 and ActiveScaffold was somewhere before beta. The project went great and everyone was happy. They’ve been using the app now for a year, managing scheduling of over 600 people for close to 1,000 events.
Recently the client came back with some requests for changes and I figured it was a good time to upgrade versions. I didn’t make a rookie mistake though and upgrade all of the versions on my MacBook Pro development box. No, in that way lies madness. You’ll goof up the Rails upgrade and all of a sudden you have no way to develop. Instead I decided to use my OpenSolaris test server (faithfully running in the basement, right next to the weight bench and the Christmas decorations) and a specially created Container just for this purpose.
What follows is a record of my experiment. I’ll spoil the surprise–I got it to work! But I uncovered a few things along the way that surprised me and I wanted to share. Doubtless one of the gurus at Sun (or even Joyent I reckon) could have short circuited some of the problems I encountered, but to be honest I had a pretty good time here. Well, except when it came time to building Mongrel with its dependent C code using friends, but that’s for later on.
A Blurb about OpenSolaris, Zones
So I’m using OpenSolaris here for the test. I develop on a MacBook Pro running Leopard, and the production server is actually Ubuntu Linux. So, why OpenSolaris?
A while ago for Bravadosoft I started researching the right way to manage our on-demand infrastructure. Joyent quickly came to my attention and I loved the way they created a scalable hosting environment for companies like ours. They use OpenSolaris primarily because of ZFS and Zones, and after a bit of playing around I decided that it worked for us. The Bravadosoft Socialytics infrastructure runs on Joyent, with a dedicated cluster per customer implementation. We can scale it up or down with a few emails to Joyent.
Anyway, I built a test server in my basement to play around with for testing, and now it’s got a new job.
Step 1: Creating a Zone
There are a few good tutorials on the web for doing this. Essentially what we want to do is create a dedicated ZFS file system for our Rails development environment, and then create the zone within it.
Michael Ernest has a post that concisely describes some of these steps.
First, create a space for the zone files to live. 2GB is plenty for our purposes. And we’ll use the “-n” space to make it a simple reservation–it doesn’t actually take all of this space until it’s needed.
-bash-3.2$ su Password: # cd / # ls bin home opt system boot kernel platform tmp catalog lib proc usr dev lost+found root var devices media sbin etc mnt second_root export net socialytics_zones # mkfile -n 2G /rails_zone # ls -lh /rails_zone -rw------T 1 root root 2.0G Jun 18 13:01 /rails_zone
Now, create the actual pool for this machine:
# zpool create rails_dev /rails_zone # zpool list NAME SIZE USED AVAIL CAP HEALTH ALTROOT rails_dev 1.98G 92.5K 1.98G 0% ONLINE - socialytics_zones 9.94G 825M 9.13G 8% ONLINE -
You can see that I have a 10GB pool in there for the Socialytics zones (I have an entire test infrastructure built) and the new 2GB pool for this effort.
Solaris also knows about it as a file system:
# df -h Filesystem size used avail capacity Mounted on /dev/dsk/c0d0s0 15G 6.3G 8.4G 43% / /devices 0K 0K 0K 0% /devices /dev 0K 0K 0K 0% /dev ctfs 0K 0K 0K 0% /system/contract proc 0K 0K 0K 0% /proc mnttab 0K 0K 0K 0% /etc/mnttab swap 4.9G 1.0M 4.9G 1% /etc/svc/volatile objfs 0K 0K 0K 0% /system/object sharefs 0K 0K 0K 0% /etc/dfs/sharetab /usr/lib/libc/libc_hwcap2.so.1 15G 6.3G 8.4G 43% /lib/libc.so.1 fd 0K 0K 0K 0% /dev/fd swap 4.9G 40K 4.9G 1% /tmp swap 4.9G 40K 4.9G 1% /var/run /dev/dsk/c0d0s4 15G 15M 15G 1% /second_root /dev/dsk/c0d0s7 427G 10G 413G 3% /export/home socialytics_zones 9.8G 19K 9.0G 1% /socialytics_zones socialytics_zones/soc_complete 9.8G 799M 9.0G 9% /socialytics_zones/soc_complete rails_dev 2.0G 1K 2.0G 1% /rails_dev
Now, create a specific file system for this container, right there in the pool:
# zfs create rails_dev/zone1 # zfs list NAME USED AVAIL REFER MOUNTPOINT rails_dev 129K 1.95G 18K /rails_dev rails_dev/zone1 18K 1.95G 18K /rails_dev/zone1 socialytics_zones 825M 8.98G 19K /socialytics_zones socialytics_zones/soc_complete 824M 8.98G 799M /socialytics_zones/soc_complete socialytics_zones/soc_complete@pre-boot 25.0M - 540M - # chmod -R 700 /rails_dev
Now let’s build the Zone. Do this with the zonecfg command and just follow the prompts:
# zonecfg -z railsdev1 railsdev1: No such zone configured Use 'create' to begin configuring a new zone. zonecfg:railsdev1> create zonecfg:railsdev1> set autoboot=false zonecfg:railsdev1> set zonepath=/rails_dev/zone1 zonecfg:railsdev1> info zonename: railsdev1 zonepath: /rails_dev/zone1 brand: native autoboot: false bootargs: pool: limitpriv: scheduling-class: ip-type: shared inherit-pkg-dir: dir: /lib inherit-pkg-dir: dir: /platform inherit-pkg-dir: dir: /sbin inherit-pkg-dir: dir: /usr zonecfg:railsdev1> add net zonecfg:railsdev1:net> set physical=rge0 zonecfg:railsdev1:net> set address=192.168.1.40 zonecfg:railsdev1:net> end zonecfg:railsdev1> verify zonecfg:railsdev1> commit zonecfg:railsdev1> exit
With the Zone now ready to go, let’s install its OS and any shared files:
# zoneadm -z railsdev1 install cannot create ZFS dataset rails_dev/zone1: dataset already exists Preparing to install zone . Creating list of files to copy from the global zone. Copying <16490> files to the zone. Initializing zone product registry. Determining zone package initialization order. Preparing to initialize <1323> packages on the zone. Initializing package <16> of <1323>: percent complete: 1%
That will take some time, YMMV, etc. After it’s finished let take a snapshot. ZFS is cool in that you can take a snapshot of a filesystem at any time, and later roll back to it or mount it as an independently running file system somewhere. You might do this, for example, if you later wanted to create another Rails development machine and test load balancing.
# zfs snapshot rails_dev/zone1@pre-boot # zfs list NAME USED AVAIL REFER MOUNTPOINT rails_dev 617M 1.35G 19K /rails_dev rails_dev/zone1 617M 1.35G 617M /rails_dev/zone1 rails_dev/zone1@pre-boot 0 - 617M - socialytics_zones 825M 8.98G 19K /socialytics_zones socialytics_zones/soc_complete 824M 8.98G 799M /socialytics_zones/soc_complete socialytics_zones/soc_complete@pre-boot 25.0M - 540M -
Ok, let’s get this machine configured:
# zoneadm -z railsdev1 boot # zlogin -e @ -C railsdev1 [Connected to zone 'railsdev1' console]
You go through the standard Solaris initialization prompts here. I chose the pretty typical options. Have your DNS servers handy. I can never get it to just find the DNS from the network. After the entire process you should get:
System identification is completed. rebooting system due to change(s) in /etc/default/init
So, it will reboot and then you see the login:
SunOS Release 5.11 Version snv_87 64-bit Copyright 1983-2008 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. Hostname: railsdev1 Reading ZFS config: done. railsdev1 console login: root Password:
How cool is that? A new server running on the host. After you login, make sure the network actually got configured correctly:
# ping -s reddit.com PING reddit.com: 56 data bytes 64 bytes from 64-215-156-99.eosinc.net (64.215.156.99): icmp_seq=0. time=20.136 ms 64 bytes from 64-215-156-99.eosinc.net (64.215.156.99): icmp_seq=1. time=22.219 ms 64 bytes from 64-215-156-99.eosinc.net (64.215.156.99): icmp_seq=2. time=21.403 ms
I figure if we can see Reddit we’re doing just fine.
Logout of the console, go back to the host, and see that the zone is running:
# @. [Connection to zone 'railsdev1' console closed] # zoneadm list -v ID NAME STATUS PATH BRAND IP 0 global running / native shared 2 railsdev1 running /rails_dev/zone1 native shared
This server is now running on your network. Now go back to it and create a non-su user:
useradd mattc (with all the typical flags you like for home directories, passwords, etc.)
Rails
Now let’s make sure Ruby is there.
$ ruby --version ruby 1.8.6 (2007-09-23 patchlevel 110) [i386-solaris2.11]
Let’s install rubygems:
$ wget http://rubyforge.org/frs/download.php/35283/rubygems-1.1.1.tgz wget: not found
Uh oh, wget isn’t there. It is, but we just don’t have it in our path. Let’s fix that by making this our .profile:
PATH=/opt/csw/bin:$PATH export PATH $ . ./.profile
Now let’s install ruby gems:
$ wget http://rubyforge.org/frs/download.php/35283/rubygems-1.1.1.tgz --2008-06-19 09:39:56-- http://rubyforge.org/frs/download.php/35283/rubygems-1.1.1.tgz Resolving rubyforge.org... 205.234.109.19 Connecting to rubyforge.org|205.234.109.19|:80... connected. HTTP request sent, awaiting response... 302 Found Location: http://rubyforge.rubyuser.de/rubygems/rubygems-1.1.1.tgz [following] --2008-06-19 09:39:56-- http://rubyforge.rubyuser.de/rubygems/rubygems-1.1.1.tgz Resolving rubyforge.rubyuser.de... 80.237.222.133 Connecting to rubyforge.rubyuser.de|80.237.222.133|:80... connected. HTTP request sent, awaiting response... 200 OK Length: 423308 (413K) [application/x-tgz] Saving to: `rubygems-1.1.1.tgz' 100%[=======================================>] 423,308 189K/s in 2.2s 2008-06-19 09:39:59 (189 KB/s) - `rubygems-1.1.1.tgz' saved [423308/423308] $ gtar -xzf *.tgz # ruby setup.rb install -c -m 0644 ubygems.rb /usr/ruby/1.8/lib/ruby/site_ruby/1.8/ubygems.rb install -c -m 0644 rubygems/server.rb /usr/ruby/1.8/lib/ruby/site_ruby/1.8/rubygems/server.rb /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1262:in `initialize': Read-only file system - /usr/ruby/1.8/lib/ruby/site_ruby/1.8/rubygems/server.rb (Errno::EROFS) from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1262:in `open' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1262:in `copy_file' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1261:in `open' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1261:in `copy_file' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:463:in `copy_file' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:844:in `install' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1395:in `fu_each_src_dest' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1411:in `fu_each_src_dest0' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1393:in `fu_each_src_dest' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:840:in `install' from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1527:in `install' from setup.rb:113 from setup.rb:108:in `each' from setup.rb:108 from setup.rb:105:in `chdir' from setup.rb:105
Uh oh, it’s a read only file system. Remember that Zones are by default using a shared, read-only /usr file system. That turns out to be a hassle, no two ways about it. It significantly lessened the fun of this venture.
Let’s first create a place for it in our home directory, by simply creating an environment variable:
$ cat .profile PATH=.:/opt/csw/bin:$PATH export PATH PREFIX=$HOME/ruby GEM_HOME=$PREFIX/lib/ruby/gems/1.8 export GEM_HOME RUBYLIB=$PREFIX/lib/ruby:$PREFIX/lib/site_ruby/1.8 export RUBYLIB
Now we install, and you’ll see that the setup routine creates the paths as necessary:
$ ruby setup.rb --prefix=$PREFIX mkdir -p /export/home/mattc/ruby/lib mkdir -p /export/home/mattc/ruby/bin install -c -m 0644 ubygems.rb /export/home/mattc/ruby/lib/ubygems.rb mkdir -p /export/home/mattc/ruby/lib/rubygems ... ------------------------------------------------------------------------------
RubyGems installed the following executables:
/export/home/mattc/ruby/bin/gem
At this point I went through an hour of struggles, with errors like:
no such file to load -- rubygems/exceptions
I fixed this by simply doing an update on the host’s gems:
# gem update --systemwhich fixed it. Honestly it’s the little stuff.
Ok now let’s install Rails.
$ gem install rails --include-dependencies INFO: `gem install -y` is now default and will be removed INFO: use --ignore-dependencies to install only the gems you list Bulk updating Gem source index for: http://gems.rubyforge.org/ Bulk updating Gem source index for: http://gems.rubyforge.org/ Successfully installed rake-0.8.1 Successfully installed activesupport-2.1.0 Successfully installed activerecord-2.1.0 Successfully installed actionpack-2.1.0 Successfully installed actionmailer-2.1.0 Successfully installed activeresource-2.1.0 Successfully installed rails-2.1.0 7 gems installed Installing ri documentation for rake-0.8.1... Installing ri documentation for activesupport-2.1.0... Installing ri documentation for activerecord-2.1.0... Installing ri documentation for actionpack-2.1.0... Installing ri documentation for actionmailer-2.1.0... Installing ri documentation for activeresource-2.1.0... Installing RDoc documentation for rake-0.8.1... Installing RDoc documentation for activesupport-2.1.0... Installing RDoc documentation for activerecord-2.1.0... Installing RDoc documentation for actionpack-2.1.0... Installing RDoc documentation for actionmailer-2.1.0... Installing RDoc documentation for activeresource-2.1.0...
Ok, can we run rails now?
$ rails rails: not found
Nope, need to add to path. Here’s what the profile says afterwards:
PATH=.:$HOME/bin:$HOME/ruby/bin:$PREFIX/lib/ruby/gems/1.8/bin:/opt/csw/bin:$PATH
Ok, let’s create a test app:
$ rails testapp
create
create app/controllers
...And now it should run. I don’t have to tell you how excited I was as I typed in these keys:
$ cd testapp $ script/server > Booting WEBrick... > Rails 2.1.0 application started on http://0.0.0.0:3000 > Ctrl-C to shutdown server; call with --help for options
And from my Mac:

Yes! An unqualified success. The hard work pays off, a Heineken is opened, my hand rises to the air in triumph, and then I forget that I’m using Mongrel on this project. Well let’s just install Mongrel!
$ gem install mongrel -include-dependencies Bulk updating Gem source index for: http://gems.rubyforge.org/ Building native extensions. This could take a while... ERROR: Error installing mongrel: ERROR: Failed to build gem native extension. /usr/ruby/1.8/bin/ruby extconf.rb install mongrel -include-dependencies creating Makefile make /opt/SUNWspro.40/SS11/bin/cc -I. -I/usr/ruby/1.8/lib/ruby/1.8/i386-solaris2.11 -I/usr/ruby/1.8/lib/ruby/1.8/i386-solaris2.11 -I. -DTEXT_DOMAIN="" -I/builds2/sfwnv-gate/proto/root_i386/usr/sfw/include -I/builds2/sfwnv-gate/proto/root_i386/usr/include -I/builds2/sfwnv-gate/proto/root_i386//readline-5.2/include -KPIC -xO3 -xbuiltin=%all -xinline=auto -xprefetch=auto -xdepend -KPIC -c fastthread.c sh: /opt/SUNWspro.40/SS11/bin/cc: not found *** Error code 1 make: Fatal error: Command failed for target `fastthread.o'
And that, dear friends, is where the fun kinda fizzled. I like Mongrel. I like Zed Shaw, and his rant remains at the same time the funniest and potentially most career limiting blog post I’ve ever read. But the fun stopped when I tried to get Mongrel running on OpenSolaris from within a zone.
The problem, to boil down a couple hours of frustration to one blog post section, is that Sun sets up the C compiler infrastructure in weird places, at least to the Fastthread and HTTPI gems that Mongrel uses. There are a few posts (here and here) out there on this, but I eventually ran out of steam. Besides I’d accomplished what I set out to do. Eventually my app ported to Rails 2.1 and ActiveScaffold 1.1.1 just fine, the client was happy, and I was (again) drinking Heineken.
Lessons
-
- ZFS and Zones are great. I didn’t really show it in this post but you can take snapshots, go to another machine, run several at once, you name it. Makes it pretty easy really.
- The read-only /usr path really gets annoying. I need to research a bit to find out how to most effectively circumvent that, without losing its benefits.
- I apparently would have made this easier on myself had I installed Sun Studio, which has the C compiler and the related tools.
- Somebody ought to do a package for Rails / Mongrel specifically for Zones. Maybe I will eventually.
- Always use the -e @ (or some other character) when doing zlogin via ssh. If you don’t you’ll have the standard ~. escape sequence, and that will exit the connection to the host machine.
I think the next post will get into my Python / Pylons zone I used for Socialytics. I’ve generally found that the packages and installation routines for Python-based tools are a bit more baked so lets give it a whack.
Read more from the Infrastructure, Rails category. If you would like to leave a comment, click here: . or stay up to date with this post via RSS, or you can
Trackback from your site.
Leave a Comment
1 Comment so far
[...] I wrote about a Rails development container I kinda left off with “it all worked and everyone’s [...]