Erik Dahlstrand

Writing on web development and server management.

Rails Server Running Multiple Ruby Versions

I recently set up a new Rails production server based on Ubuntu 10.04, Nginx, Unicorn and rbenv. A few of my Rails applications is still running on Ruby 1.8.7 so the web server must handle multiple ruby versions.

The idea is that each and every Rails application should be self contained. Meaning that running unicorn uses the Ruby in .rbenv-version and the Unicorn in Gemfile.lock. By telling Bundler to generate binstubs we know that the gems installed within the application will be used. Bundler should be the only gem installed to the global gemset.

This how-to assumes a vanilla Ubuntu with Nginx installed. Please have a look at my how-tos for setting up Ubuntu and install Nginx. Capistrano is used to deploy the Rails application. I will only show you the changes necessary to the deploy file. If you are unfamiliar with Capistrano you can read more about it on the Capistrano website.

To keep it really simple I give the deployer user sudo rights. That is not necessary and probably not recommendable. But in this how-to I want to focus on the key parts to get the server up and running.

Deploy User

I’m using a single account for all my application deployments. Some people likes to have a separate user account for each application.

Create an deployer user account and add it to the staff group.

1
useradd -m -g staff -s /bin/bash deployer

Give this user a password:

1
passwd deployer

Now open the sudoers file and make the staff user group able to perform a sudo by adding the following line to it:

/etc/sudoers
1
%staff ALL=(ALL) ALL

Upload your public ssh-key for password-less login to the deployer account. You can find more detailed instructions in the Ubuntu how-to:

1
scp ~/.ssh/id_rsa.pub deployer@server:.ssh/authorized_keys

With the user account created, you should login to the server as deployer.

Install Ruby

Install the dependencies:

1
2
aptitude install git-core
aptitude install build-essential openssl libreadline6 libreadline6-dev curl zlib1g zlib1g-dev libssl-dev libyaml-dev libxml2-dev libxslt-dev autoconf libc6-dev ncurses-dev automake libtool bison

Install rbenv:

1
2
3
4
cd
git clone git://github.com/sstephenson/rbenv.git .rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

Install ruby-build:

1
2
3
4
cd
git clone git://github.com/sstephenson/ruby-build.git
cd ruby-build
sudo ./install.sh

Install Rubies:

1
2
3
rbenv install 1.9.3-p0
rbenv install ree-1.8.7-2011.03
...

Configure RubyGems

Add to .gemrc

~/.gemrc
1
2
3
---
install: --no-ri --no-rdoc --env-shebang
update: --no-ri --no-rdoc --env-shebang

Rails Application Setup

I start with an existing Rails application called myapp. I will only show you the modifications neccessary to make the application deployable on Nginx/Unicorn with rbenv. Don’t forget to check in the new files created below.

The application is accessible at http://myapp.com.

Create a .rbenv-version file with a Ruby version of choice:

1
echo '1.9.3-p0' > .rbenv-version

Add Unicorn to the Gemfile and run bundle install:

Gemfile
1
2
3
group :production do
  gem 'unicorn'
end

Create a unicorn.rb configuration file:

config/unicorn.rb
1
2
3
4
5
6
7
8
9
10
11
12
worker_processes 2
user 'deployer', 'staff'

preload_app true
timeout 30

working_directory "/home/deployer/myapp_com/current"
listen "/tmp/app.socket", :backlog => 64

pid "/home/deployer/myapp_com/current/pids/unicorn.pid"
stderr_path "/home/deployer/myapp_com/current/log/unicorn.stderr.log"
stdout_path "/home/deployer/myapp_com/current/log/unicorn.stdout.log"

The deploy.rb file must be adjusted to load rbenv into the shell that Capistrano uses. We also want Bundler to generate binstubs and honor the .rbenv-version file and use the project-specified Ruby version. This allows us to switch versions of Ruby by pushing a new .rbenv-version file.

Add these lines to Capistrano deploy.rb:

config/deploy.rb
1
2
set :default_environment, { 'PATH' => "/home/deployer/.rbenv/shims:/home/deployer/.rbenv/bin:$PATH" }
set :bundle_flags, "--deployment --quiet --binstubs --shebang ruby-local-exec"

Deploy the application:

1
cap deploy

Server Setup

On the server we need to create a Nginx Virtual Host file for myapp.

Requests that cannot be handled by Nginx web server are passed on to the Unicorn application server. Beacuse they are on the same machine the best communication method is through a Unix domain socket.

/etc/nginx/sites-available/myapp_com
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
upstream myapp_com {
  server unix:/tmp/myapp_com.socket fail_timeout=0;
}

server {
  server_name www.myapp.com;
  rewrite ^ http://myapp.com$uri permanent;
}

server {
  server_name myapp.com;
  root /home/deployer/myapp_com/current/public;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    include proxy_params;
    proxy_redirect off;

    if (!-f $request_filename) {
      proxy_pass http://myapp_com;
      break;
    }
  }
}

Enable the site by creating a symbolic link in sites-enabled and restart nginx:

1
2
3
cd /etc/nginx/sites-enabled
ln -s ../sites-available/myapp_com
service nginx restart

We also need an init file to startup the myapp Unicorn instance:

/etc/init.d/unicorn_myapp_com
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#! /bin/sh

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn web server
# Description:       starts unicorn
### END INIT INFO

PATH=/home/deployer/.rbenv/bin:/home/deployer/.rbenv/shims:$PATH
DAEMON=/home/deployer/myapp_com/current/bin/unicorn
DAEMON_OPTS="-c /home/deployer/myapp_com/current/config/unicorn.rb -E production -D"
NAME=unicorn
DESC=unicorn
PID=/home/deployer/myapp_com/shared/pids/unicorn.pid

case "$1" in
  start)
  echo -n "Starting $DESC: "
  $DAEMON $DAEMON_OPTS
  echo "$NAME."
  ;;
  stop)
  echo -n "Stopping $DESC: "
        kill -QUIT `cat $PID`
  echo "$NAME."
  ;;
  restart)
  echo -n "Restarting $DESC: "
        kill -QUIT `cat $PID`
  sleep 1
  $DAEMON $DAEMON_OPTS
  echo "$NAME."
  ;;
  reload)
        echo -n "Reloading $DESC configuration: "
        kill -HUP `cat $PID`
        echo "$NAME."
        ;;
  *)
  echo "Usage: $NAME {start|stop|restart|reload}" >&2
  exit 1
  ;;
esac

exit 0

Make the init script executable and then start the application:

1
2
3
4
chmod +x unicorn_myapp_com
service unicorn_myapp_com start

update-rc.d unicorn_myapp_com defaults # start application on server boot

Provided that your application has been successfully deployed and available at /home/deployer/myapp_com/current/ the Unicorn application should start. If the application fails to start please make sure the PID path exists.

1
/home/deployer/myapp_com/shared/pids/

Done!

Comments