Nick not found

Switching From RVM to rbenv on a Production Server

Mar 21 2013
I recently switched a mission-critical production server to use rbenv instead of RVM. The server has been around a little over two years, and it was configured at the beginning to use RVM for hosting one primary and a couple supporting Rails apps.

RVM did its job well, but it always felt heavy handed and overly complex, especially for a server.

I heard about rbenv when it first started making the rounds in 2011, and it got my interest, but since I was using RVM locally and on the server, I wasn’t able to try it out without some destructive changes to my environments (since the two don’t play nice together).

A few weeks ago I was planning to update to Ruby 1.9.3 and Rails 3.2. Being a full-time developer, I don’t have as much time as I’d like to tend to the servers. However, I decided that since I was doing some spring cleaning anyway, I’d go ahead switch to rbenv, both locally and on the server.

The decision was not made lightly. After all, RVM was doing its job just fine. However, I never understood how RVM worked internally, other than that it did some magic by replacing the cd command with a function. I took a little time trying to understand it, but I just couldn’t get there.

rbenv, on the other hand, is lightweight and very simple to understand. You add a couple entries to your $PATH, and rbenv updates symlinks so that the active Ruby version is on your path without all the magic.


The Process

Here I’ll walk through the process step by step, showing the commands I entered and config changes I made to switch from RVM to rbenv.

First, remove RVM and all traces from the system. This is the scary part, because there’s no way to undo. Sure, you could reinstall RVM, but it will likely be a more up-to-date version than what you had.

I originally installed RVM on the server in multiuser mode, so I need to use rvmsudo here. (sudo wouldn’t work because rvm was not on my $PATH.) Running rvm implode is used to uninstall RVM. In addition, you’ll need to remove some references manually that implode doesn’t take care of.

# uninstall rvm and its supporting files
$ rvmsudo rvm implode

# remove rvmrc file for multiuser installs
$ sudo rm /etc/rvmrc

# remove the 'rvm' group
$ sudo delgroup rvm

# remove 'rvm' references from .bashrc, .bash_profile, .profile, etc.
$ vim ~/.bashrc

At this point, we need to start a new shell (e.g., a new ssh session) so that we’re completely rid of RVM (remember, RVM modified our cd command, and uninstalling it won’t remove the effects of the command proxy until logging in again).

Next, follow the rbenv install instructions:

$ git clone git://github.com/sstephenson/rbenv.git ~/.rbenv

# add ~/.rbenv/bin to the $PATH and `rbenv init` to the shell
# (use ~/.profile instead of ~/.bash_profile on Ubuntu)
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

# start a new shell
$ exec $SHELL -l

Next, install ruby-build as a plugin to rbenv.

$ git clone git://github.com/sstephenson/ruby-build.git \
            ~/.rbenv/plugins/ruby-build

Now we can install Ruby.

$ rbenv install 1.9.3-p392
# rebuild the rbenv shims
$ rbenv rehash

# optionally use this new version as the global default
$ rbenv global 1.9.3-p392

# make sure the global setting stuck
$ which ruby
# => /home/admin/.rbenv/shims/ruby
$ ruby -v
# => ruby 1.9.3p392 (2013-02-22 revision 39386) [x86_64-linux]

Rails

After getting RVM removed and rbenv installed, I needed to update the Rails apps. To be on the safe side, I removed the bundle directory for each app (where Bundler keeps the gems in a standard deployment with Capistrano) so I could install a fresh copy of the gems under the new version of Ruby (as I had just upgraded from 1.9.2 to 1.9.3).

And when I say ‘removed’, I mean that I moved the directories to a place the app wasn’t looking for them, just in case.

# install gem bundler
gem install bundler
rbenv rehash

$ mkdir -p ~/tmp/old-bundles
# for each app: back up old bundles and reinstall
$ mv ~/apps/myapp/shared/bundle ~/tmp/old-bundles/myapp
$ cd ~/apps/myapp/current
$ bundle install --path ~/apps/myapp/shared/bundle \
                 --deployment --quiet \
                 --without development test

# make sure the app can at least load in console mode
$ cd ~/apps/myapp/current
$ bundle exec rails console production

Cron

As with many apps, I have a few cron jobs that run rake tasks inside my app as well as a few Ruby scripts for backups and the like. These used RVM, so I needed to make some updates there as well.

# first, back up the existing crontab
mkdir -p ~/tmp/crontab
crontab -l > ~/tmp/crontab/2013-03-20.txt

Next, I removed references to my custom rvm-run script and replaced them with a simple bash -lc (to load a shell with rbenv configured) and added bundle exec where necessary. (I plan to eventually use binstubs instead of having to prefix everything with bundle exec, but that’s for another day.)

First, an example of a Rake task cron job, split onto multiple lines with a backslash for readability:

# the old way, with RVM and my rvm-run script:
0 0 * * * cd /home/admin/apps/myapp/current && \
          /home/admin/scripts/rvm-run \
          "bundle exec rake app:task RAILS_ENV=production"
# the new rbenv cron job:
0 0 * * * bash -lc \
          "cd /home/admin/apps/myapp/current && \
          bundle exec rake app:task RAILS_ENV=production"

Next, an example of a Ruby script cron job, also split for readability:

# the old way, with RVM and my rvm-run script:
5 0 * * * /home/admin/scripts/rvm-run \
          "/home/admin/scripts/backup-databases"
# the new rbenv cron job:
5 0 * * * bash -lc \
          "cd /home/admin/scripts && bundle exec backup-databases"

For the record, the Deploying with rbenv wiki page makes mention of how to manually set the $PATH for cron jobs without the use of bash -lc, but I couldn’t get it to work in the admittedly few minutes I tried.

It’s also worth mentioning that I initially had trouble with cron still having a reference to RVM, so I restarted the cron daemon, which on Ubuntu is done with:

sudo service cron restart

For the sake of completeness, my rvm-run wrapper script is at the end of this post.

Unicorn

The init.d scripts I use for starting, stopping, and reloading Unicorn also referenced RVM, so they needed some attention as well.

In the unicorn script, I changed this:

CMD="rvm use 1.9.2@rails-3.0 && \
     cd $APP_ROOT && \
     rvmsudo bundle exec unicorn -c config/unicorn.rb -E $ENV -D"

to this:

PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
CMD="cd $APP_ROOT; bundle exec unicorn -c config/unicorn.rb -E $ENV -D"

After updating the Unicorn scripts, I wanted to restart the apps under Ruby 1.9.3 and using the newly installed bundle. (Remember to make sure you can launch your app in console mode before this next step.)

Unfortunately, a zero-downtime reload of an app won’t work in this case because of all the changes. When I tried that (with sudo /etc/init.d/unicorn-myapp reload), I received this error:

executing ["/home/admin/apps/myapp/shared/bundle/ruby/1.9.1/bin/unicorn",
 "-c", "config/unicorn.rb", "-E", "production", "-D"]
 (in /home/admin/apps/myapp/releases/20130301010101)
/usr/bin/env: ruby: No such file or directory
reaped #<Process::Status: pid 12345 exit 127> exec()-ed

To work around this, I stopped and then started each app:

sudo /etc/init.d/unicorn-myapp stop
sudo /etc/init.d/unicorn-myapp start

After that, I had the existing apps running under a new version of Ruby with rbenv instead of RVM.

The full unicorn script is at the end of this post.

Deploying Rails Apps with Capistrano and rbenv

I use Capistrano to deploy my Rails apps, and because it needed to be configured for RVM, I had to make a few changes to get it working with rbenv.

The deploy.rb file started like this with RVM:

# Add RVM's lib directory to the load path.
$:.unshift(File.expand_path('./lib', ENV['rvm_path']))

# Load RVM's capistrano plugin.
require "rvm/capistrano"

# set the RVM environment on the server
set :rvm_ruby_string, '1.9.2@rails-3.0'

It now starts out like this:

# add binstubs in app/bin directory
set :bundle_flags, '--deployment --quiet --binstubs'

# set up the environment for rbenv since Capistrano
# doesn't execute .profile (so it needs rbenv's $PATH)
set :default_environment, {
  'PATH' => "$HOME/.rbenv/shims:$HOME/.rbenv/bin:$PATH"
}

In addition, I had one more reference to RVM in the deploy:start task that took care of starting Unicorn. What used to be this (split onto multiple lines for readability):

task :start, roles: :app, except: { no_release: true } do
  # run a simple sudo command to force Capistrano to
  # ask for the sudo password, because rvmsudo doesn't
  # cause Capistrano to prompt for a password (this will
  # let the next 'rvmsudo' command run)
  sudo "date"
  run "cd #{current_path} && rvmsudo bundle exec unicorn " +
      "-c #{unicorn_config} -E #{rails_env} -D"
end

has been updated to this:

task :start, roles: :app, except: { no_release: true } do
  sudo "cd #{current_path} && bundle exec unicorn " +
       "-c #{unicorn_config} -E #{rails_env} -D"
end

Both of these depend on unicorn_config being set in the config:

set :unicorn_config, "#{current_path}/config/unicorn.rb"

A Cleaner, Leaner Server with rbenv

As I said, RVM was working fine and had been for a long time, and I thank its authors and contributors for all the hard work that went into it.

That being said, I’m glad I took the time to switch to rbenv, which is a lot leaner and easier to understand. These were the two main factors in my decision to switch, and for me, they were worth it.

I feel more confident now that I know how Ruby is being made available to the apps and scripts on my server.


unicorn init.d script

Here is the Unicorn init.d script I use for Rails apps. It has pieces taken from lots of different sources.

#!/bin/sh

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn app server
# Description:       starts unicorn using start-stop-daemon
### END INIT INFO

set -e

USAGE="Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"

# app settings
USER="admin"
APP_NAME="myapp"
APP_ROOT="/home/$USER/apps/$APP_NAME/current"
ENV="production"

# environment settings
PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
CMD="cd $APP_ROOT; bundle exec unicorn -c config/unicorn.rb -E $ENV -D"
PID="$APP_ROOT/tmp/pids/unicorn.pid"
OLD_PID="$PID.oldbin"

# make sure the app exists
cd $APP_ROOT || exit 1

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
  test -s $OLD_PID && kill -$1 `cat $OLD_PID`
}

case $1 in
  start)
    sig 0 && echo >&2 "Already running" && exit 0
    echo "Starting $APP_NAME"
    su - $USER -c "$CMD"
    ;;
  stop)
    echo "Stopping $APP_NAME"
    sig QUIT && exit 0
    echo >&2 "Not running"
    ;;
  force-stop)
    echo "Force stopping $APP_NAME"
    sig TERM && exit 0
    echo >&2 "Not running"
    ;;
  restart|reload|upgrade)
    sig USR2 && echo "reloaded $APP_NAME" && exit 0
    echo >&2 "Couldn't reload, starting '$CMD' instead"
    $CMD
    ;;
  rotate)
    sig USR1 && echo rotated logs OK && exit 0
    echo >&2 "Couldn't rotate logs" && exit 1
    ;;
  *)
    echo >&2 $USAGE
    exit 1
    ;;
esac

rvm-run script

I know it’s possible to accomplish this now with what RVM has built in, but when I wrote this script, this functionality either wasn’t available or I simply couldn’t find it, so I wrote my own. I’m including it here only for the sake of completeness.

#!/bin/bash
if [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
  source "/usr/local/rvm/scripts/rvm"
  if [ -f gemset ] ; then
    rvm 1.9.2@`cat gemset`
  elif [ -f config/gemset ] ; then
    rvm 1.9.2@`cat config/gemset`
  else
    rvm 1.9.2@scripts
  fi
  $1
else
  echo "ERROR: An RVM installation was not found."
fi

Four Article Comments

Vignesh

Jun 3 '16 at 11:19 pm

Hi,
Thank you for the post.

I have followed the steps, but for my case, the ruby version(2.1.5) which I mentioned in the deploy.rb is not taking while doing cap deploy. It’s taking the rbenv global ruby version(1.8). I can’t change the rbenv global version, because other applications are using this global version. Can you suggest me any other solution is available for that.



Dave Morse

Apr 22 '17 at 09:00 pm

I’m really suspicious of the line in this script

 ENV="production"

I’m pretty sure it should be

 RAILS_ENV="production"

I used a similar script, and everything seemed fine, but unicorn had no idea it was production because the environment wasn’t set correctly.



Leave a Comment * indicates a required field


(Do not fill out this field, or your comment will be ignored. This field is here to help us protect against automated comments.

Your name, URL, and comment will appear on this page after I have reviewed it. (I do this to prevent spam). Some Markdown is allowed. I may make formatting adjustments to your comment. Your email address will not be published.

RSS

« Back to all articles

More articles about:


Lightrail Online Platform

Lightrail – a solid, secure platform for insurance websites.

Find out what makes Lightrail awesome »


A little bit about me

© 2017 nicknotfound.com