Peter Marklund

Peter Marklund's Home

Fri Oct 05 2007 04:01:46 GMT+0000 (Coordinated Universal Time)

Ruby on Rails Deployment on FreeBSD

I did a Ruby on Rails FreeBSD deployment for a client the other day and I thought I'd share what I learned in an instructional format here. Previously I had mostly deployed on Linux (Suse, CentOS etc.) and I was curious to see what the FreeBSD experience would be like. I googled for instructions and immediately found the RailsOnFreeBSD page in the Rails Wiki. Other than that I couldn't find much Ruby on Rails and FreeBSD specific instruction out there. Note - most of the instructions in this post are not specific to FreeBSD but are generic Ruby on Rails deployment steps for Unix.

We were migrating from Windows to FreeBSD and the goal was to eliminate single points of failure. We settled on two application servers with FreeBSD 6.2 on HP hardware, both running the web and app tiers in the vanilla Rails deployment stack, i.e. Apache 2.2.4, mod_proxy_balancer, Mongrel cluster, and Mongrel. A load balancer external to the Rails system would load balance between the two Apache servers. The database we use is MySQL 5 and it sits on a separate server. The idea is to add another db server with some form of MySQL replication. We have yet to decide which replication to use and recommendations are welcome. For deployment we use Capistrano 2.

The first thing I do on a FreeBSD server is to log in with the root user and change shell from C shell to bash:

cd /usr/ports/shells/bash
make install clean
chsh -s /usr/local/bin/bash root
exit
su -
echo $SHELL

As a personal preference (or ignorance of vi maybe), I install Emacs. This is a good time to go grab a cafe latte, since the installation takes forever:

cd /usr/ports/editors/emacs
make install clean

We then add the user that Capistrano will log in as and that Mongrel will run under - the deploy user:

# Make sure to choose the bash shell. You can keep the defaults for most of the other questions.
adduser deploy

To be able to deploy with Capistrano without repeatedly being prompted for a password, we set up public/private key authentication:

# On the production server:
ssh-keygen -t rsa
# On your development server:
ssh-keygen -t rsa
scp ~/.ssh/id_rsa.pub deploy@production-server:.ssh/remote_key
ssh deploy@production-server
cd .ssh
cat remote_key >> authorized_keys
rm remote_key
exit
# Now ssh should not prompt for a password:
ssh deploy@production-server

We edit ~/.bashrc and setup the environment for the deploy user. I think it's important to set RAILS_ENV to production. I configure the bash prompt and the history size (the number of shell commands listed by the history command) and my preferred editor. I also add some convenient aliases for accessing the log file and mysql:

export RAILS_ENV=production

export PS1="[\u@\h:\w] "
export HISTSIZE=10000
export EDITOR=emacs

export PATH=$PATH:/usr/local/mysql/bin

export APP="/var/www/apps/streamio/current"
alias cdapp='cd $APP'
alias logapp='tail -f $APP/log/production.log'
alias restartapp='cdapp; mongrel_rails cluster::restart -C config/mongrel_cluster.yml'
alias mysqlapp='mysql -h db.host.name -u db.user -pdb-password database-name'

To make sure the ~/.bashrc file is sourced, edit or create ~/.profile and add the following line to it:

source ~/.bashrc

We install sudo and give the deploy user sudo access. That way we can use sudo from Capistrano to restart the Apache web server that will be running as root:

# Install sudo
cd /usr/ports/security/sudo
make install clean
emacs /usr/local/etc/sudoers
# Uncomment wheel group
pw user mod deploy -G wheel

Make sure the clock on the server is in sync by invoking "crontab -e" and add this line:

*/30 * * * * /usr/sbin/ntpdate ntp1.sp.se

The above line syncs the clock every half hour with an internet clock - an ntp server. In this case we use ntp1.sp.se, but you may choose a different npt server that is available in your country.

Now, finally, the time has come for us to install Ruby on Rails which is really the heart of our server (or where our hearts are as Rails developers at least). As indicated in the Rails wiki, we can use the rubygem-rails port for this. The port will install both Ruby (the programming language), RubyGems (the package manager for Ruby software), and Ruby on Rails (the web framework). The portinstall command in the Rails wiki didn't work for me, so I used make install instead:

# Update the ports tree - takes a long time...
portsnap fetch ; portsnap extract

# Install Ruby, RubyGems, and Rails
cd /usr/ports/www/rubygem-rails
make install clean

# Check your version of the installed software
# The versions given here are the ones I got, you may find later versions
ruby -v
=> ruby 1.8.5 (2006-08-25) [i386-freebsd6]
gem -v
=> 0.9.2
rails -v
=> Rails 1.2.3

Now that we have RubyGems at our fingertips, we can install Capistrano, Mongrel Cluster, and Mongrel in a snap:

gem install capistrano -y
gem install mongrel_cluster -y
cap --version
=> Capistrano v2.0.0
mongrel_rails --version
=> ** Ruby version is not up-to-date; loading cgi_multipart_eof_fix
=> Mongrel Web Server 1.0.1

To be able to control the version and the configuration I chose to install Apache from source, and I followed the instructions in the excellent Mongrel book by Zed Shaw:

mkdir /usr/local/src
cd /usr/local/src
# Visit http://httpd.apache.org and download httpd-2.2.4.tar.gz
tar xzf httpd-2.2.4.tar.gz 
cd httpd-2.2.4
./configure --enable-proxy --enable-proxy-balancer --enable-proxy-http --enable-rewrite \
--enable-cache --enable-headers --enable-ssl
make
make install

# You can check the location of the httpd binary:
/usr/libexec/locate.updatedb
locate httpd|grep bin

Add the Apache startup script:

emacs /etc/rc.conf
# Add the following line:
httpd_enable="YES"
ln -s /usr/local/apache2/bin/apachectl /usr/local/etc/rc.d/httpd

# Start Apache
/usr/local/etc/rc.d/httpd start

# Fetch http://production.host.name in a browser. You should see the text "It Works".

We now configure Apache for our Mongrel server like it says in the Mongrel book:

cd /usr/local/apache2/conf
emacs httpd.conf
# Add one line: 
Include /usr/local/apache2/conf/rails.conf

Create the /usr/local/apache2/conf/rails.conf file with contents like this (make sure to query replace $app_name$ with the name of your Rails app, i.e. the basename of your RAILS_ROOT):

NameVirtualHost *:80
# Setup the cluster
<Proxy balancer://$app_name$_cluster>
  BalancerMember http://127.0.0.1:8000
  BalancerMember http://127.0.0.1:8001
  BalancerMember http://127.0.0.1:8002
</Proxy>

<VirtualHost *:80>
  ServerAdmin $email$
  ServerName localhost
  ServerAlias localhost
  DocumentRoot /var/www/apps/$app_name$/current/public
  <Directory '/var/www/apps/$app_name$/current/public'>
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
  </Directory>

  ProxyPass / balancer://$app_name$_cluster/
  ProxyPassReverse / balancer://$app_name$_cluster/

  RewriteEngine On

  RewriteRule ^/$ /index.html [QSA]

  RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
  RewriteRule ^/(.*)$ balancer://$app_name$_cluster%{REQUEST_URI} [P,QSA,L]

  ErrorLog /var/www/apps/$app_name$/current/log/apache_error_log
  CustomLog /var/www/apps/$app_name$/current/log/apache_access_log combined
</VirtualHost>

At this point it makes sense to restart Apache to make sure that the config file parses. We now finish up on the server by installing Subversion, MySQL client, ImageMagick, and RMagick:

cd /usr/ports/devel/subversion
make install clean

cd /usr/ports/databases/mysql50-client/
make install clean

cd /usr/ports/graphics/ImageMagick
make install clean

gem install rmagick -y

If you are in a Windows environment you may want to install Samba:

cd /usr/ports/net/samba3
make install clean

You should now have the mount_smbfs command available for mounting Windows disks on your FreeBSD server.

We are now just about ready to deploy to our FreeBSD server using Capistrano 2 from our development machine. Before we do that though, let's create the directory on the FreeBSD server we'll be deploying to:

mkdir /var/www
chown deploy /var/www

Now, make sure your database.yml is properly configured. Make sure you can connect to MySQL from the FreeBSD servers. Capify your Rails app if you haven't already:

cd RAILS_ROOT
capify .

You now need to edit config/deploy.rb to fit your server. In particular, make sure you have the deploy_to variable set to "/var/www/apps/#{application}" and you have the proper roles set up. Here is a sample:

role :web, rails01, rails02
role :app, rails01, rails02
role :db,  rails01, :primary => true
role :scm, rails01

Also, make sure to define the deploy:restart task to restart using Mongrel Cluster. Also, symlink in any shared files in a callback. Here is a sample from my deploy.rb file to get you started (don't copy it in it's entirety, that won't work):

namespace :deploy do  
  # ===========================================================================
  # Mongrel
  # ===========================================================================  
  def mongrel_cluster(command)
    "cd #{current_path} && " +
    "mongrel_rails cluster::#{command} -C #{current_path}/config/mongrel_cluster.yml"
  end

  %w(restart stop start).each do |command|
    task command.to_sym, :roles => :app do
      run mongrel_cluster(command)
    end
  end

  # ===========================================================================
  # Apache
  # ===========================================================================  

  desc "Restart Apache web server"
  task :restart_web do
    sudo "/usr/local/etc/rc.d/httpd restart"
  end

  # ===========================================================================
  # Deployment hooks
  # ===========================================================================  

  desc "Copy in server specific configuration files"
  task :copy_shared do
    proxy_dir = "#{release_path}/vendor/plugins/reverse_proxy_fix/lib"
    run <<-CMD
    cp #{release_path}/config/database.yml.example #{release_path}/config/database.yml &&
    cp #{release_path}/config/directories.rb.example #{release_path}/config/directories.rb &&
    cp #{proxy_dir}/config.rb.unix #{proxy_dir}/config.rb
    CMD
  end

  desc "Run pre-symlink tasks" 
  task :before_symlink, :roles => :web do
    copy_shared
    backup_db
    run_tests
  end

  desc "Run the full tests on the deployed app." 
  task :run_tests do
    run "cd #{release_path} && RAILS_ENV=production rake && cat /dev/null > log/test.log" 
  end

  desc "Clear out old code trees. Only keep 5 latest releases around"
  task :after_deploy do
    cleanup
    sleep 5
    ping_servers
  end

If you have your deploy.rb in check, you should now be able to run "cap deploy:setup" to setup the Capistrano directory structure on the servers, and finally the magic command to deploy to both of the FreeBSD servers:

cap deploy

Good luck!