Deploying Rails with Mongrels on a Shared Web Server

Rails probably won’t be the next big thing. With behemoths like ASP.NET offering tight integration with existing desktop apps and household names like PHP now bundled with every Linux distribution, Rails doesn’t have enough to offer the run-of-the-mill web developer to win them over.

But some know.

Once you’ve learned Rails (and thus Ruby), you think of web application development in a whole new way. A way where anything’s possible, and changing the design on the fly does not have to be your most annoying pet peeve.

Those who know are enjoying it–an API that works harder than they do. If you’re writing your app from the ground up, choosing Rails will be guaranteed good experience and an experience of good. No, it won’t be easy. Ruby isn’t a simple language to pick up. But the API documentation is *great* and the language offers a flexibility and semantic clarity that is unrivalled.

What is rivalled, however, is deployment. Most of the popular languages of this day have plugins that allow them to run threads right in the web server, saving copious amounts of time precious time that would be spent with CGI forking, executing the interpreter and loading up the libraries. Rails, if you want to use it with Apache, requires either you use CGI (which is fatally slow) or have access to the server-wide configuration to set up a proxy balancer.

These days, running one’s own server is just not practical. To run your own server from your home is a real headache–limited upload bandwidth (on cable), service agreement issues–and the need to buy business-class connections to do it legally, and lest we forget, the complete pain of being responsible for keeping the server up and running. Sure, you could buy a virtual server, but there are issues there as well, not the least of which is forking out $20-$30/mo for a mere 10GB of server space–and hardly enough memory to serve a single website with reasonable response.

No, these days, shared web servers are where it’s @. Dreamhost (http://www.dreamhost.com) offers for $6-$11 500GB of space with 5TB of transfer per month, and once you sign up, you earn 2GB of storage and 40GB of transfer a week! With deals like this, it’s silly to pay $25/mo for the benefit of having access to the server-wide configuration.

It does pose an issue for Rails, however. The Rails recommended practice is to use mongrels to serve up your page. Without the ability to create a balancer in Apache, it’s not possible to directly coerce Apache into utilizing more than a single mongrel–which would have terrible performance (though still better than CGI). Perhaps FastCGI would be a possible alternative, but that method is soon-to-be deprecated. If you want to do it the right way, you need to use mongrels.

Open Source to the Rescue
One intruiging fact about many shared web servers is that they allow you to compile and run your own packages. In fact, I prefer to deploy Rails using my own ruby and gems. I created a ~/.packages directory and used it as the configure prefix, and then added it first on my path (PATH=~/.packages/bin:$PATH). Having your own local binaries of the important things adds a great deal of security to your situation–it feels good to know that your provider can’t update anything and throw off your app. I guess in the case of gems, not freezing is asking for trouble, but just the same, having your own local ruby conveys a warm, fuzzy feeling.

So after using CGI for a day, something had to change. FastCGI wasn’t working on the server (a shared web server from JaguarPC), and I didn’t feel like trying to work that issue out through support tickets. Since FastCGI still wasn’t the right way to do it, I was trying to figure out how I could make mongrels work without trying to get JagPC to create a load balancer (which they probably wouldn’t do anyway).

Then I remembered an open source app: balance. Balance a user-space TCP load balancer. It takes all of the options on the command line and just does its job. Compile it up and run it. It has proved very effective.

The Strategy
The idea will be this: spawn 5 mongrels starting on port 61501. Spawn balance on 127.0.0.1:61500 with servers at 127.0.0.1:61501-61505. Then, through a .htaccess file and mod_rewrite, pass all of the requests through to 127.0.0.1:61500 with rewrite flag P (to proxy). It’s a cinch!

My deployment has a bit more complexity, as I am running three environments (DEV, TEST, and PROD) off of a single codebase (three copies, but all are checked out of the same SVN repository). I wanted to keep the .htaccess files in the repository, so I needed a single version that could differentiate between the environments. Not so hard, actually.

.htaccess

# General Apache options
AddHandler fastcgi-script .fcgi
AddHandler cgi-script .cgi
Options +FollowSymLinks +ExecCGI

# If you don't want Rails to look in certain directories,
# use the following rewrite rules so that Apache won't rewrite certain requests
#
# Example:
#   RewriteCond %{REQUEST_URI} ^/notrails.*
#   RewriteRule .* - [L]

# Redirect all requests not available on the filesystem to Rails
# By default the cgi dispatcher is used which is very slow
#
# For better performance replace the dispatcher with the fastcgi one
#
# Example:
#   RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On

# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
#   Alias /myrailsapp /path/to/myrailsapp/public
#   RewriteBase /myrailsapp
RewriteRule ^$ index.html [QSA]
#RewriteRule ^([^.]+)$ $1.html [QSA]

#Rewrite for Production environment
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{SERVER_NAME} (.*)my.domain
RewriteCond %1 !(test|dev)
RewriteRule ^(.*)$ http://127.0.0.1:61500/$1 [QSA,P]

#Rewrite for Test environment
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{SERVER_NAME} (.*)my.domain
RewriteCond %1 test
RewriteRule ^(.*)$ http://127.0.0.1:61600/$1 [QSA,P]

#Rewrite for Development environment
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{SERVER_NAME} (.*)my.domain
RewriteCond %1 dev
RewriteRule ^(.*)$ http://127.0.0.1:61700/$1 [QSA,P]

#RewriteRule ^(.*)$ dispatch.cgi [QSA,L]

# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be rendered instead
#
# Example:
#   ErrorDocument 500 /500.html

ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

The file is based off of the one that is supplied by Rails. It handles the separation of the environments pased on HTTP host header name sent by the client. The dev environment is served from dev.my.domain, test from test.my.domain and prod from my.domain.

I also created a simple script to handle starting and stopping the mongrels (listed at the end of this article). If I need to change the number of mongrels for each environment, I simply change the variables at the top of the script and restart the environment.

One last script I wrote runs from cron (which many shared web servers will also give you access too) to make sure that the mongrels and balancers are up. It’s simple enough that I won’t list it here. All it does is check for the processes, and if any are down it runs the above script to restart all of the environments.

servers.sh (starts and stops mongrels and balance instances)

#!/bin/bash
. ~/.bash_profile
PROD_PORT=61500
TEST_PORT=61600
DEV_PORT=61700
PROD_PATH="~/Projects/MyApp/Production"
TEST_PATH="~/Projects/MyApp/Test"
DEV_PATH="~/Projects/MyApp/Development"
NUM_MONGRELS_PROD=5
NUM_MONGRELS_TEST=2
NUM_MONGRELS_DEV=2

function usage {
echo "usage: servers.sh start|stop production|test|development|all"
}

function stop_production {
PIDM=`ps aux | grep `whoami` | grep mongrel | grep production | awk '{ print $2}'`
PIDB=`ps aux | grep `whoami` | grep balance | grep $PROD_PORT | awk '{ print $2}'`
if [[ -n "$PIDM" || -n "$PIDB" ]];
then
kill  $PIDM $PIDB
fi
rm -f "$PROD_PATH/log/mongrel.pid"
}

function stop_test {
PIDM=`ps aux | grep `whoami` | grep mongrel | grep test | awk '{ print $2}'`
PIDB=`ps aux | grep `whoami` | grep balance | grep $TEST_PORT | awk '{ print $2}'`
if [[ -n "$PIDM" || -n "$PIDB" ]];
then
kill  $PIDM $PIDB
fi
rm -f "$TEST_PATH/log/mongrel.pid"
}

function stop_development {
PIDM=`ps aux | grep `whoami` | grep mongrel | grep development | awk '{ print $2}'`
PIDB=`ps aux | grep `whoami` | grep balance | grep $DEV_PORT | awk '{ print $2}'`
if [[ -n "$PIDM" || "$PIDB" ]];
then
kill  $PIDM $PIDB
fi
rm -f "$DEV_PATH/log/mongrel.pid"
}

function start_production {
#Start Production
mongrel_rails start -d -n $NUM_MONGRELS_PROD -e production -c "$PROD_PATH" -p $(($PROD_PORT+1))
MONGRELS=""
for i in `seq -s " " $(($PROD_PORT+1)) $(($PROD_PORT+$NUM_MONGRELS_PROD))`
do
MONGRELS="127.0.0.1:$i $MONGRELS"
done
balance $PROD_PORT $MONGRELS
echo -n production
}

function start_test {
#Start Test
MONGRELS=""
mongrel_rails start -d -n $NUM_MONGRELS_TEST -e test -c "$TEST_PATH" -p $(($TEST_PORT+1))
for i in `seq -s " " $(($TEST_PORT+1)) $(($TEST_PORT+$NUM_MONGRELS_TEST))`
do
MONGRELS="127.0.0.1:$i $MONGRELS"
done
balance $TEST_PORT $MONGRELS
echo -n " test"
}

function start_development {
#Start Development
MONGRELS=""
mongrel_rails start -d -n $NUM_MONGRELS_DEV -e development -c "$DEV_PATH" -p $(($DEV_PORT+1))
for i in `seq -s " " $(($DEV_PORT+1)) $(($DEV_PORT+$NUM_MONGRELS_DEV))`
do
MONGRELS="127.0.0.1:$i $MONGRELS"
done
balance $DEV_PORT $MONGRELS
echo -n " development"
}

case $1 in
start)
case $2 in
production)
stop_production
start_production;
;;
test)
stop_test
start_test
;;
development)
stop_development
start_development
;;
all)
stop_production
start_production
stop_test
start_test
stop_development
start_development
;;
*)
usage
;;
esac
;;
stop)
case $2 in
production)
stop_production
;;
test)
stop_test;
;;
development)
stop_development
;;
all)
stop_production
stop_test
stop_development
;;
*)
usage
;;
esac
;;
*)
usage
;;
esac
echo

There are no comments on this post

Leave a Reply