Deployment Strategy for Rails, Passenger and Nginx server with Multiple Virtual Hosts

In this blog post i intend to share notes taken during deployment exercise of rails apps on Multiple Virtual Hosts.

I have decided to put this all down for self reference and possibly interesting debates that may help expose areas that i am not be aware of.

Deploying rails apps from a beginners perspective (like mine) could be really confusing.

This note will show how i have come to avoid breaking the passenger-nginx compilation and deploying rails apps to Multiple Virtual Hosts.

The development stack/environment is based on the following technologies:

  • Centos
    • Run these commands to find out what version of Centos your server is.
    • `cat /etc/redhat-release` gives me: “CentOS Linux release 6.0 (Final)”
    • `uname -a` gives me: “Linux servername-xxx 2.6.32-71.29.1.el6.x86_64 #1 SMP Mon Jun 27 19:49:27 BST 2011 x86_64 x86_64 x86_64 GNU/Linux”
  • Nginx Web Server
  • RVM 1.10.0
  • Phusion Passenger 3.0.11
  • Ruby 1.9.2 patch level 290
  • Rails 3.1.3

It is worthy of note that at the time of writting this post the urls and rpm are valid. if you encounter any not found kinda error, you can google to find the rpm and the include it to the centos repo file.

sudo vim /etc/yum.repos.d/CentOS-Base.repo  

Firstly, before we get started we need to setup the following

Setup a deployment user account
Our rails application will run as user: deployerbot.
To create a deployerbot, allocate it to a group, modify the deployerbot user password and then add deployerbot to sudoers file.

groupadd staff # if staff does not exist (cat /etc/groups | grep staff)
useradd -m -g staff -s /bin/bash deployerbot # if you need to add deploybot user to more groups  usermod -G grp1,grp2,newgrp username
passwd deployerbot
visudo  # edit to include deployerbot to the sudoers file.. notice that i changed this to visudo instead of vim /etc/sudoers.. this is a safer way to edit sudoers. see this link for more on visudo http://linux.about.com/library/cmd/blcmdl8_visudo.htm.. thanks to Geoff Low (gflow) for the correction.. 

# include this line below  'root ALL=(ALL) ALL': to enable deployerbot to use the sudo command
deployerbot ALL=(ALL) ALL
#or alternatively add the staff group below  'root ALL=(ALL) ALL': to enable deployerbot to use the sudo command as a member of the group
%staff ALL=(ALL) ALL

Now the web applications are going to run in production-mode, include this line to /etc/environment to avoid repeating it for every kind of Rails like commands:

RAILS_ENV=production

Install RVM
Make sure you have all the essential armoury (dependencies) :)

yum groupinstall "Development Tools"  # ubuntu equivalent may be this apt-get install build-essential ruby-full libmagickcore-dev imagemagick libxml2-dev libxslt1-dev git-core ruby-devel libxml2 libxml2-devel libxslt libxslt-devel

yum install kernel-devel kernel-headers # this may already have been installed by default

yum install openssl-devel libcurl-devel ImageMagick httpd-devel ruby-libs zlib-devel libjpeg-devel giflib-devel readline-devel

# or this could also work as well if the above fails

sudo rpm -Uvh http://download.fedora.redhat.com/pub/epel/5/i386/epel-release-5-4.noarch.rpm  

yum -y install libpcap libpcap-devel libnet libnet-devel pcre pcre-devel gcc automake autoconf libtool make gcc-c++ libyaml libyaml-devel zlib zlib-devel pkgconfig ruby-devel libxml2 libxml2-devel libxslt libxslt-devel

Check out this link for more information

To Install RVM on Centos this following command is require (do this as the deployment user). Also make sure you already have curl and git installed.

bash < <(curl -s https://rvm.beginrescueend.com/install/rvm) # after installation, rvm should be ready to use.. if not reboot

rvm notes # to find out if there are any system specific setting that require attention.. for centos you may need to create a .screenrc file with only this as its content shell -/bin/bash

usermod -a -G staff,rvm deployerbot  #modify deployerbot to multiple groups rvm and staff

For Centos depending on the version you are running on i would advise run the command below for more information

rvm notes

Install Ruby and Upgrade RubyGems
For precautionary reason install 1.9.2 with the following packages: (do this as the deployment user)


rvm pkg install readline # you may want to do (--skip-autoreconf) this for each package to avoid autoreconf error: rvm --skip-autoreconf pkg install readline 
rvm pkg install zlib
rvm pkg install openssl
rvm install 1.9.2 --with-openssl-dir=$rvm_path/usr --with-readline-dir=$rvm_path/usr # with zlib optional rvm install 1.9.2 --with-zlib-dir=$rvm_path/usr --with-openssl-dir=$rvm_path/usr --with-readline-dir=$rvm_path/usr
rvm use 1.9.2 # to make this the system's ruby do: rvm --default use 1.9.2

# Upgrade RubyGems to the latest (stable) and greatest.. :)
wget http://rubyforge.org/frs/download.php/xxxxx/rubygems-x.x.x*.tgz
tar zxvf rubygems-x.x.x*.tgz
cd rubygems-x.x.x*
ruby setup.rb
# to confirm rubygem upgrade:
gem -v

Create a Gemsets for each web application
This blog focuses on deployment on multiple virtual hosts we will be creating two gemsets demonstration purposes.

rvm gemset create webapp1
rvm gemset create webapp2

On the server create a ~/.gemrc file

:verbose: true
:bulk_threshold: 1000
install: --no-ri --no-rdoc --env-shebang
:sources:
- http://gemcutter.org
- http://gems.rubyforge.org/
- http://gems.github.com
:benchmark: false
:backtrace: false
update: --no-ri --no-rdoc --env-shebang
:update_sources: true


To deploy these apps on Nginx

Install and setup passenger and nginx
Passenger as i have come to know is simply mod_rails pr mod ruby for webservers like nginx.
now here is where i have observed that i made a big mistake. Now because you have multiple virtual host sitting on the server, they are mostly likely not useing the exactly set of gems and even possibly may different rubies.

With RVM installed on the server, your RVM directory is located here “$HOME/.rvm/” probably has taken this form.

cd $HOME/.rvm/gems/
ls

# archives  environments  help     log        README   tmp      wrappers
# bin       examples      hooks    man        rubies   user
# config    gems          lib      patches    scripts  usr
# contrib   gemsets       LICENCE  patchsets  src      VERSION

Out of all these we are interested in two folders the gems and wrapper.

A brief look into the gems folder you will find the following

cd gems
ls

# cache            ruby-1.9.2-p290@webapp1      ruby-1.9.2-p290@webapp2
# ruby-1.9.2-p290  ruby-1.9.2-p290@global  

Because we want to maintain virtual host and accommodate dissimilarities between webapp1 and webapp2

I suggest installing passenger in the ruby-1.9.2-p290 gemset folder. This will avoid breaking passenger on each deployment of any of the two web applications, because for each time you reload or reinstall your gems , there is a big propensity of getting the passenger-nginx link broken. Hence the need to recompile passenger for nignx on the server.

This could also lead to errors like:

  • 500 internal server error
  • 403 Forbidden
  • [Tue Dec 27 22:43:57 2011] [error] *** Passenger could not be initialised because of this error: Unable to start the Phusion Passenger watchdog because it encountered the following error during startup: Unable to start the Phusion Passenger logging agent because its executable (/path/to/gems/passenger-3.0.11/agents/PassengerLoggingAgent) doesn’t exist. This probably means that your Phusion Passenger installation is broken or incomplete. Please reinstall Phusion Passenger

So i think it’s better to have the passenger gem separate from the gemsets for the web application. This will not require recompiling passenger on every deployment, hence leaving a high chance of downtime problems.

Now i have come across solutions like unicorn, but for now what is obtain in my environment is passenger. Burt will be checking unicorn out pretty soon.

To achieve this do the following (perform this as the deployment user):

rvm use 1.9.2 # load the ruby-1.9.2-p290 folder into your current session

gem install passenger # this should install passenger to the ruby-1.9.2-p290 folder

passenger-install-nginx-module # do this to setup/compile passenger with nginx

At a certain stage of the passenger-install-nginx-module you will be asked to chose to between two setup processes. For me i will take the number 1 option of installing nginx from scratch which is the recommended option. This also makes sense since so far we have not installed nginx.

Nginx Virtual Host Configuration
For nginx assuming that decided to install it in /etc/nginx/ passenger will locate the nginx.conf in the conf folder. To correctly link passenger and nginx the copy the following to your nginx.conf file

user  deployerbot staff;

worker_processes  2;

events {
    worker_connections  1024;
}
http {
    passenger_root /usr/local/rvm/gems/ruby-1.9.2-p290/gems/passenger-3.0.11;
    passenger_ruby /usr/local/rvm/wrappers/ruby-1.9.2-p290/ruby;

    include       mime.types;
    default_type  application/octet-stream;

    access_log  /etc/nginx/logs/access.log;
    error_log  /etc/nginx/logs/error.log;

    sendfile        on;    
    keepalive_timeout  65;    
    include /etc/nginx/conf.d/*.conf;
}

Before the next steps create the conf.d directory. I personal prefer to keep them inside conf.d. You can chosen to keep yours somewhere else only remember to link it up.

mkdir conf.d

Now for webapp1 you want to configure you server something like this

server {
    # if you're running multiple servers, instead of "default" you should
    # put your main domain name here
    listen 80;
	
    # you could put a list of other domain names this application answers
    server_name webapp1.com;

    root /var/www/path/to/webapp1/current/public;
    access_log /var/www/path/to/webapp1/logs/webapp1.access.log;
    error_log /var/path/to/webapp1/logs/webapp1.error.log;

    rewrite_log on;

    passenger_enabled on;
}

And for webapp2 we have

server {
    # if you're running multiple servers, instead of "default" you should
    # put your main domain name here
    listen 80;
	
    # you could put a list of other domain names this application answers
    server_name webapp2.com;

    root /var/www/path/to/webapp2/current/public;
    access_log /var/www/path/to/webapp2/logs/webapp2.access.log;
    error_log /var/path/to/webapp2/logs/webapp2.error.log;

    rewrite_log on;

    passenger_enabled on;
}

Obviously the above steps do not settle it all. We need to create our web app.

rails new webapp1 
rails new webapp2

Create a ~/.rvmrc to trust your .rvmrc project files and (create) load the project specific gemset

rvm_trust_rvmrcs_flag=1

if [[ -s "/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1" ]] ; then
  . "/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1"
else
  rvm --create use  "1.9.2@webapp1"
fi

Here comes another part.

Unlike Apache’s SetEnv feature, nginx does not allow setting of environment variable rather relies on the shell for environment variables.

Passenger on the other hand is a shel script and will use the default settings of the shell if the rails application does not specify its own settings.

To avoid this problem, you need to set environment variable for each application (webapp1 and webapp2).

To do this edit config/production.rb with the following:


WebApp1::Application.configure do
# Settings specified here will take precedence over those in config/application.rb

  # Code is not reloaded between requests
  config.cache_classes = true #..........
# .....................................
end

#paste this at the bottom and edit this part with your own relevant paths

ENV['GEM_HOME']='/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems' # take note for webapp2 it will be /usr/local/rvm/gems/ruby-1.9.2-p290@webapp2/gems
ENV['GEM_PATH']='/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems:/usr/local/rvm/gems/ruby-1.9.2-p290@global/gems'
ENV['PATH']='/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/bin:$PATH'
ENV['MY_RUBY_HOME']='/usr/local/rvm/wrappers/ruby-1.9.2-p290@webapp1/ruby'

The above will set the environment variable for the rails application to the gemset folder /usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems as well as all others.

Next you need to create a in the config folder setup_load_paths.rb this is well explained in this post
Copy and paste the following:

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

# Pick the lines for your version of Bundler
# If you're not using Bundler at all, remove all of them

# Require Bundler 1.0
ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'

# Require Bundler 0/9
# if File.exist?(".bundle/environment.rb")
#   require '.bundle/environment'
# else
#   require 'rubygems'
#   require 'bundler'
#   Bundler.setup
# end

Now last part is the Capistrano recipe to compliment the above setting.

# First of all create capistrano files with this
capify .

Following the above copy this part to the Capfile:

# this part goes in the Capfile which is located in the root of your rails app

# 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 it to the ruby + gemset of your app, e.g:
set :rvm_ruby_string, '1.9.2@webapp1'

#this part goes in the deploy.rb file inside the config in your rails app

require 'bundler/capistrano'
set :application, "webapp1.com"

set :domain, "www.domain.com"
set :environment, "production"
set :branch, "master"
set :deploy_to, "/var/www/path/to/www/webapp1"

role :app, domain
role :web, domain
role :db, domain, :primary => true

default_run_options[:pty] = true

default_run_options[:shell] = 'bash'

default_environment["RAILS_ENV"] = 'production'

set :repository, "git@ domain.com/webapp1.git"
set :deploy_via, :remote_cache

# If you aren't using Subversion to manage your source code, specify
# your SCM below:
set :scm, :git
set :scm_verbose, true
set :use_sudo, false
set :ssh_options, :forward_agent => true

set :user, "deployerbot"
set :keep_releases, 7

set :default_environment, {
    'PATH' => "/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems/bin:$PATH",
    'RUBY_VERSION' => 'ruby 1.9.2',
    'GEM_HOME' => '/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems',
    'GEM_PATH' => '/usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems:/usr/local/rvm/gems/ruby-1.9.2-p290@global/gems',
    'BUNDLE_PATH' => '//usr/local/rvm/gems/ruby-1.9.2-p290@webapp1/gems' # If using bundler.
}

namespace :deploy do
  desc "restarting"
  task :restart do
    run "touch #{current_path}/tmp/restart.txt"
  end

  desc "symlink vendor to shared vendor"
  task :symlink_vendor_to_shared_vendor do
    run "ln -s #{current_path}/../shared/vendor #{current_path}/vendor"
  end

  desc "trust rvmrc"
  task :trust_rvmrc do
    run "rvm rvmrc trust #{release_path}"
  end
end

after 'deploy:symlink', 'deploy:symlink_vendor_to_shared_vendor', 'deploy:trust_rvmrc'

Next do

cap deploy:setup 
cap deploy:cold
cap deploy

On the server you want to load each application’s gemset and do bundle install

rvm use 1.9.2@webapp1
cd /var/www/path/to/www/webapp1
bundle install


rvm use 1.9.2@webapp2
cd /var/www/path/to/www/webapp2
bundle install

Do keep in mind, that if you start nginx manually, including the sudo -E will inform sudo to preserve your environment. If this is not done, sudo will possibly reset all your environment variables.

cd /etc/nginx/sbin/
sudo -E ./nginx

After doing this, nginx is sure to use the your chosen set of configuration.

Here ends the deployment notes, please feel free to comment below.

And away we go ! Ciao for now..

Advertisement

2 thoughts on “Deployment Strategy for Rails, Passenger and Nginx server with Multiple Virtual Hosts

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s