Greg Benedict

Thoughts on the web and creativity.

Hosting Rails Apps Using Nginx and Mongrel Cluster

While I had never been happy with the performance of Apache, Lighttpd and FastCGI for hosting Rails, it was in fact a more pressing need that caused me to find something faster. See, there is this little known fact that FastCGI caches ranges for model validations in Rails.

But caching is good isn’t it?
Well, not in this case.

The range I was validating against was a dynamic date range. Specifically, will something occur in the next six months. This is where the caching became an issue. The to and from components of that range are dynamic, but regardless of when I did the check they were always the same as the day FastCGI was restarted (6 months after that day for the high end). This did not show up in development using lighttpd, webrick or mongrel. I also tried to change it using a custom validation, but that failed as well.

Everything I had read to this point showed Apache –> Lighttpd –> FastCGI as the standard. It’s just what people used. It’s what we used.

As I began my search for a solution, Google quickly turned me to nginx via a post on Err The Blog which pointed me in the direction of Geoffrey Grosenbach. Yeah, the same guy makes those excellent PeepCode screencast tutorials and does the Ruby on Rails Podcast. The Err The Blog posting also pointed me to Ezra’s new love of nginx. He’s been using it for rails hosting at EngineYard. So now I’ve found 3 blogs that I read using it and loving it.

Nginx is the brain child of Igor Sysoev and has been in use by over 20% of all Russian websites for several years now. How does that fly under the radar???

In case you are wondering it’s pronounced Engine X.

Installing nginx
I’m using Centos 4.4 on my testing server, but the install is fairly straight forward.

  1. mkdir /usr/local/nginx
  2. cd /usr/local/nginx
  3. wget http://sysoev.ru/nginx/nginx-0.5.22.tar.gz
  4. tar -xvzf nginx-0.5.22.tar.gz
  5. ./configure && make && make install

Installing Mongrel and Mongrel Cluster
This assumes you have ruby 1.85 and ruby gems already installed.

  1. sudo gem install mongrel
  2. sudo gem install mongrel_cluster
  3. sudo cp /usr/lib/ruby/gems/1.8/gems/mongrel_cluster-0.2.1/resources/mongrel_cluster /etc/init.d/mongrel_cluster
  4. add a PATH statement to mongrel_cluster file just above the CONF_DIR variable:PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local:/usr/local/sbin:/usr/local/bin
  5. sudo chmod +x /etc/init.d/mongrel_cluster
  6. sudo chown -R <group>:<user> /var/www/domains/<sitename>/current/
  7. sudo chown -R <group>:<user> /var/www/phpmyadminI have phpMyAdmin installed to make things easier with all this testing. It also shows you how to run php under nginx at the same time.

Configuring nginx

I borrowed an nginx init.d script from TopFunky. I suggest you do the same. This allows you to start nginx as a service using

service nginx start

Here is my nginx configuration script (nginx.conf)

user railswww railswww;

worker_processes 1;

pid /var/run/nginx.pid;

# Valid error reporting levels are debug, notice and info
error_log logs/error.log debug;

events {
worker_connections 1024;
}

http {

# pull in mime-types. You can break out your config
# into as many include’s as you want to make it cleaner
include /usr/local/nginx/conf/mime.types;

# set a default type for the rare situation that
# nothing matches from the mimie-type include
default_type application/octet-stream;

# configure log format
log_format main ‘$remote_addr – $remote_user [$time_local] ‘
‘”$request” $status $body_bytes_sent “$http_referer” ‘
‘”$http_user_agent” “$http_x_forwarded_for”‘;

# main access log
access_log /var/log/nginx_access.log main;

# main error log
error_log /var/log/nginx_error.log debug;

# no sendfile on OSX
sendfile on;

# These are good default values.
tcp_nopush on;
tcp_nodelay off;
# output compression saves bandwidth
gzip on;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_proxied any;
gzip_types text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# gzip_min_length 1100;
# gzip_buffers 4 8k;

# the server directive is nginx’s virtual host directive.
server {
# port to listen on. Can also be set to an IP:PORT
listen 80;

# sets the domain[s] that this vhost server requests for.
# None means listen to all.
server_name test1.tgfi.net;

# Set the max size for file uploads to 50Mb
client_max_body_size 50M;

# Replace with the full path to your phpmyadmin directory:
root /var/www;

# vhost specific access log
access_log /var/log/nginx.test1.tgfi.net.access.log main;

index index.html index.htm index.php;

# this rewrites all the requests to the maintenance.html
# page if it exists in the doc root. This is for capistrano’s
# disable web task
if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}

location / {
# Uncomment to allow server side includes so nginx can
# post-process Rails content
## ssi on;

# needed to forward user’s IP address to rails
proxy_set_header X-Real-IP $remote_addr;

# needed for HTTPS
proxy_set_header X_FORWARDED_PROTO https;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;

# If the file exists as a static file serve it directly without
# running all the other rewite tests on it
if (-f $request_filename) {
break;
}

# check for index.html for directory index
# if its there on the filesystem then rewite
# the url to add /index.html to the end of it
# and then break to send it to the next config rules.
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}

# Look for existence of PHP index file.
# Don’t break here…just rewrite it.
if (-f $request_filename/index.php) {
rewrite (.*) $1/index.php;
}
}

# Requires you to start one instance of http://topfunky.net/svn/shovel/nginx/php-fastcgi.sh
location ~ \.php$ {
fastcgi_pass 127.0.0.1:8888;
fastcgi_index index.php;

fastcgi_param SCRIPT_FILENAME /var/www/$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
}

}

# The following includes are specified for virtual hosts
include /var/www/domains/domain1/current/config/nginx.conf;
include /var/www/domains/domain2/current/config/nginx.conf;
include /var/www/domains/domain3/current/config/nginx.conf;
}

You’ll notice at the very end I reference an included conf file from each rails project (deployed via Capistrano). This makes config a snap and I only have to make a change in one place on the server to include the file. Here is the virtual host config file:

# The name of the upstream server is used by the mongrel
# section below under the server declaration
upstream mongrel_domain1 {
server 127.0.0.1:8200;
server 127.0.0.1:8201;
server 127.0.0.1:8202;
}

server {
# port to listen on. Can also be set to an IP:PORT
listen 80;

# Set the max size for file uploads to 50Mb
client_max_body_size 50M;

# sets the domain[s] that this vhost server requests for.
# None means listen to all.
server_name domain1.tgfi.net;

# doc root
root /var/www/domains/domain1/current/public;

# vhost specific access log
access_log /var/www/domains/domain1/shared/log/nginx.access.log main;

# this rewrites all the requests to the maintenance.html
# page if it exists in the doc root. This is for capistrano’s
# disable web task
if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}

location / {
# Uncomment to allow server side includes so nginx can
# post-process Rails content
## ssi on;

# needed to forward user’s IP address to rails
proxy_set_header X-Real-IP $remote_addr;

# needed for HTTPS
#proxy_set_header X_FORWARDED_PROTO https;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;

# If the file exists as a static file serve it directly without
# running all the other rewite tests on it
if (-f $request_filename) {
break;
}

# check for index.html for directory index
# if its there on the filesystem then rewite
# the url to add /index.html to the end of it
# and then break to send it to the next config rules.
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}

# Look for existence of PHP index file.
# Don’t break here…just rewrite it.
if (-f $request_filename/index.php) {
rewrite (.*) $1/index.php;
}

# this is the meat of the rails page caching config
# it adds .html to the end of the url and then checks
# the filesystem for that file. If it exists, then we
# rewite the url to have explicit .html on the end
# and then send it on its way to the next config rule.
# if there is no file on the fs then it sets all the
# necessary headers and proxies to our upstream mongrels
if (-f $request_filename.html) {
rewrite (.*) $1.html break;
}

# You’ll need to change this proxy_pass to match what
# what you specified above. It must be unique to each vhost.

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

error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/www/domains/domain1/current/public;
}

# Pass the PHP scripts to FastCGI server listening on ip:port.
#
# Requires you to start one instance of http://topfunky.net/svn/shovel/nginx/php-fastcgi.sh
location ~ \.php$ {
fastcgi_pass 127.0.0.1:8888;
fastcgi_index index.php;

fastcgi_param SCRIPT_FILENAME /var/www/domains/domain1/current/public$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
}

}

Configuring Mongrel Cluster
Mongrel Cluster is a wraper to easily setup load balanced sites of multiple mongrel servers. It can easiloy fire up using a yaml file which you can put inside each rails project’s config directory. This will start 3 servers listening on ports 8200 – 8202 (referenced in the nginx vhost config file).

cwd: /var/www/domains/domain1/current
port: 8200
environment: production
user: railswww
group: railswww
address: 127.0.0.1
pid_file: /var/www/domains/domain1/shared/pids/mongrel.pid
servers: 3

Whenever you deploy you have Capistrano do a restart (or a start on cold deploy) of Mongrel Cluster.

set :mongrel_config, “/var/www/domains/#{application}/current/config/mongrel_cluster.yml”

desc “Restart mongrel_cluster(which restarts rails)”
#namespace :deploy do
task :restart do
sudo “mongrel_rails cluster::restart -C #{mongrel_config}”
end
#end

desc “Cold deploy start mongrel_cluster(which restarts rails)”
task :start do
sudo “mongrel_rails cluster::start -C #{mongrel_config}”
end

Up and Running
Once I got nginx and mongrel cluster up and running I could see what everyone was raving about. I always thought the speed with which Apache/Lighttpd/FastCGI served pages was a bottleneck of rails. Boy was I wrong! Immediately I saw a speed increase — a big increase. Nginx has a much higher throughput and only gets better under load.

The static file serving by nginx is an added benefit as it doesn’t have to proxy down a level to Mongrel to read a html, css, jpg, gif, etc. file. Anything cached by Rails gets put into /public as an html file. After the first hit and cache file creation, nginx just knows to pick it up.

Conclusion
The combination of nginx and Mongrel Cluster fixed my date range caching issue, but I’m much more excited about the new found speed our rails apps have. Hats of to Igor Sysoev for an excellent piece of work.

If you’ve got an questions, post them in the comments below, or drop me a note at gbenedict [at] gmail [dot] com.

Additional Help
Here are the links that helped me out along the way:

11 ResponsesLeave one →

  1. kritias

     /  October 2, 2007

    thanks!!

  2. really cool recipe. it worked nice for me.

    thanks

  3. Hi,

    I can’t put to work the init.d script for nginx on Centos 4.4, as you say… you could provide your script?

  4. gbenedict

     /  November 5, 2007

    Mamcx – Can you tell me what error or issue you are having?

  5. Brian

     /  January 31, 2008

    Hi,

    I’m new to Ruby, Rails, etc. I have a VPS on VPS Village. I’m trying to get a Ruby on Rails stack going. I (think) I have everything installed. I am using nginx and I can see static pages. I’m not that familiar with Mongrel … do I want to install Mongrel & Mongrel_cluster? I only have a 128M VPS so I’m trying to keep things small so I can learn Ruby on Rails and will probably upgrade to 256M later.

    Thanks for pointing a noob in the right direction.

    Regards,

    Brian

  6. Brian

     /  January 31, 2008

    …. actually I guess the first Rails app I’m trying to get to run is Mephisto. I have Rails 2.0.2 on Ruby 1.8.6.

    Right now I’m trying to figure out the Mongrel/nginx config because I think this is keeping Mephisto from running right now. I have Mephisto trunk checked out right now from subversion.

  7. gbenedict

     /  January 31, 2008

    @Brian –

    Wordpress takes far less resources to run a site than any Rails Project and can easily be run on a shared host and will fly on a VPS.

    The downside to Rails is that it is very memory intensive because of the way it caches Models and Controllers in memory. Also, you can’t process more than 1 request at a time (it’s not thread safe), so you’ll need at least 300-400MB to effectively run a small rails app with 3 mongrels.

    If you like ruby in general and want to stick with that route, you may want to take a look at Merb (http://merbivore.com/) in combination with thin (http://code.macournoyer.com/thin/). It takes less memory to run but can still use pieces of rails, such as Active Record, to keep you learning.

    Greg

  8. Brian – 128mb of ram probably won’t do it, but 256 will. There are plenty of people that use slicehost that run rails apps with 3 mongrels and have a 256mb slice.

  9. Thank you for the great write up!

    Is there any way to run multiple rails apps in subdirectories in a single domain with NGINX?
    Similar to apaches proxypass 127.0.0.1:8000/prefixname when mongrel runs with –prefix=/prefix?

  10. @Joe — You can do this by adding additional location and upstream directives for each one.

    location /subfolder1 {
    
         ...
         if (!-f $request_filename) {
              proxy_pass http://mongrel_subfolder1;
              break;
         }
    
    }
    
    upstream mongrel_subfolder1 {
         server 127.0.0.1:8300;
         server 127.0.0.1:8301;
         server 127.0.0.1:8302;
    }
    

    You’ll also probably need to make routing changes in your rails apps.

  11. In the config file :

    proxy_set_header Host $http_host:$server_port;

    instead of :

    proxy_set_header Host $http_host;

    makes it work when Nginx isn’t listening to port 80.

Leave a Reply