geek# techno-babble for the masses

Nginx and Rails and PHP, Oh My!

As you could probably guess, my blog (and several sites of my friends) are hosted on a lovely Linux VPS provided by Linode. I honestly can’t say enough nice things about the service and reliability I’ve received from Linode (and no they don’t pay me to speak highly of them!). But that’s not really the point of this post. The point is actually quite simple: My VPS doesn’t have a lot of memory, and I’m always wary of my resource consumption. A few months ago, I moved from Apache to lighttpd for this reason, alone. Let’s face it… Apache is a memory hog, and that problem is well-documented, so I won’t really go into details here. “Lighty” has served me well for the past few months, but for reasons I’m about to explain, I felt the need to move to a different platform.

So earlier this year I wrote a post about how I wanted to learn Ruby on Rails. For a while now, I’ve put that project on the back burner. I decided after my last post on the subject that ASP.NET MVC was more fitting for me because, let’s face it, I’m a .NET developer right? And aren’t .NET developers supposed to stick with .NET stuff? After deploying my first site using ASP.NET MVC, I came away with a feeling that it was a little heavier than I’d like it to be, and I really don’t like paying for another separate Windows VPS just to host one website! There’s also the fact that Linux is way more efficient in limited resource deployments (like VPS’s). All of this boils down to the fact that Rails called to me once again, and I decided that if I was going to do Rails development, I needed to figure out the deployment environment.

After doing some research, Ruby Enterprise Edition and Phusion Passenger seemed like the obvious choice for production Rails deployment. Both products value resource management in environments just like mine, and the numbers they claim to achieve were very impressive! If you want to run Phusion Passenger, however, your choices in a web server are limited to Apache or Nginx. Considering my previous encounters with Apache, I decided Nginx was the way to go. That said, after hours of fussing getting this environment set up properly, I decided that it would be best if I contributed a guide to the community. This wasn’t the easiest process in the world, and I hope this tutorial can help someone else avoid the headaches that I faced.

Step 1: Environment and pre-requisites

First and foremost, I use a 32-bit version of Ubuntu 9.10 Karmic Koala on my VPS. the instructions you see are specific to my environment, but I’m sure they could be adapted for use in others. On my server, there were some packages that were necessary pre-requisites. Here are the steps to install them:

First, edit your /etc/apt/sources.list and remove the comment marks (#) in front of the universe repository lines

deb http://us.archive.ubuntu.com/ubuntu/ karmic universe  
deb-src http://us.archive.ubuntu.com/ubuntu/ karmic universe  
deb http://us.archive.ubuntu.com/ubuntu/ karmic-updates universe  
deb-src http://us.archive.ubuntu.com/ubuntu/ karmic-updates universe  
deb http://security.ubuntu.com/ubuntu karmic-security universe  
deb-src http://security.ubuntu.com/ubuntu karmic-security universe  

Next you need to issue the following commands to get all the rest of the packages you need:

sudo apt-get update  
sudo apt-get install build-essential libxml2-dev libssl-dev libbz2-dev curl libcurl4-openssl-dev libpng12-dev libmcrypt-dev mysql-server libmysqlclient-dev libxslt1-dev autoconf2.13 libltdl-dev libreadline5-dev libsqlite3-ruby postgresql-server-dev-8.4 libpcre3-dev  

Next you need to install libevent. There is a version of this in the repository, but it’s pretty out of date, and PHP-FPM prefers the newer version. For this requirement, I decided to compile from source. Use the following commands to get this installed.

wget http://monkey.org/~provos/libevent-1.4.13-stable.tar.gz  
cd libevent-1.4.13-stable  
./configure
make && sudo make install  

Step 2: Compiling PHP from source with Suhosin and PHP-FPM

So the reason I decided to compile PHP from source is because it’s the easiest way (for me) to apply the PHP-FPM patch and make sure I have everything I need. I realize that there may be some Ubuntu packages out there at some point, but there weren’t any that were easily found when I performed these steps. That said, our first step is to snag the PHP source and then apply the Suhosin patch.

wget http://us2.php.net/get/php-5.3.1.tar.bz2/from/this/mirror  
bzip2 -dc php-5.3.1.tar.bz2 | tar xf -  
wget http://download.suhosin.org/suhosin-patch-5.3.1-0.9.8.patch.gz  
gunzip suhosin-patch-5.3.1-0.9.8.patch.gz  
patch -d php-5.3.1 -p1 < suhosin-patch-5.3.1-0.9.8.patch  

Now we have to install the PHP-FPM patch. In order to do this, we actually need a legacy version of autoconf and autoheader, because the PHP buildconf script relies on them. Luckily, they were installed earlier when we grabbed stuff from the Ubuntu repositories. All we have to do is set some environment variables to force the legacy versions to be used. Then we can just apply the patch and compile PHP.

export PHP_AUTOCONF=/usr/bin/autoconf2.13  
export PHP_AUTOHEADER=/usr/bin/autoheader2.13  
wget http://launchpad.net/php-fpm/master/0.6/%2Bdownload/php-fpm-0.6~5.3.1.tar.gz  
tar -zxf php-fpm-0.6~5.3.1.tar.gz  
php-fpm-0.6-5.3.1/generate-fpm-patch  
patch -d php-5.3.1 -p1 < fpm.patch  
cd php-5.3.1  
./buildconf --force
./configure --enable-bcmath --with-bz2 --enable-calendar --with-fpm --with-libevent=shared --with-curl --enable-dba --enable-exif --enable-ftp --with-gd --with-gettext --enable-mbstring --with-mysql --with-mysqli --with-pdo-mysql --with-openssl --with-pcre-regex --enable-shmop --enable-soap --enable-sockets --enable-sysvmsg --enable-wddx --enable-zip --with-zlib --enable-sysvsem --enable-sysvshm --with-mcrypt --enable-pcntl --enable-mbregex --with-mhash --with-xsl
sudo make all install  

Step 3: Compiling Ruby Enterprise Edition and Nginx

Now that PHP-FPM is installed, we need to get Nginx up and running. The good part is that Ruby Enterprise Edition comes bundled with Phusion Passenger which will handle compiling Nginx for us. We do, however, need to download the Nginx source manually because there are some options that we’d like to pass to the Nginx configure script.

cd ..  
wget http://sysoev.ru/nginx/nginx-0.7.64.tar.gz  
tar -zxf nginx-0.7.64.tar.gz  
wget http://rubyforge.org/frs/download.php/66162/ruby-enterprise-1.8.7-2009.10.tar.gz  
tar -zxf ruby-enterprise-1.8.7-2009.10.tar.gz  
sudo ruby-enterprise-1.8.7-2009.10/installer  

The cool part about Ruby Enterprise Edition is that it’s completely isolated from the rest of your system, and it installs most of the default gems for you. As a matter of fact, the only thing left to do is to actually install Nginx with Phusion Passenger support, and that’s exactly what we’re doing next!

sudo /opt/ruby-enterprise-1.8.7-2009.10/bin/passenger-install-nginx-module  

While running through this installer, there are a series of prompts. Be sure you answer the following:

Enter your choice (1 or 2) or press Ctrl-C to abort: 2  
Please specify the directory:  
Please specify a prefix directory [/opt/nginx]: /usr/local/nginx  
Extra arguments to pass to configure script: --sbin-path=/usr/local/sbin --with-http_ssl_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --with-http_stub_status_module --with-http_gzip_static_module  

Step 4: Configuration of PHP-FPM and Nginx

Sweet! At this point you should have a shiny new Nginx install. Before we can start it up, though, there are a few configuration files we have to mess with. First we need to configure PHP-FPM to run as the proper user and place a default php.ini file in the appropriate directory. First, let’s edit php-fpm.conf. There are 4 lines we need to change, so just search through the file and make sure they look like this:

<value name="owner">www-data</value>  
<value name="group">www-data</value>  
<value name="user">www-data</value>  
<value name="group">www-data</value>  

Next we need to copy in the default php.ini file from the source directory.

sudo cp php-5.3.1/php.ini-production /usr/local/etc/php.ini  

Finally, we need to configure Nginx. I don’t like having one huge configuration file, so I really like to split things up. You can edit this to your liking, but here’s basically how I set my default config (found at /usr/local/nginx/conf/nginx.conf):

user  www-data;  
worker_processes  6;

events {  
    worker_connections  1024;
}

http {  
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  10 10;

    passenger_root /opt/ruby-enterprise-1.8.7-2009.10/lib/ruby/gems/1.8/gems/passenger-2.2.7;
    passenger_ruby /opt/ruby-enterprise-1.8.7-2009.10/bin/ruby;

    gzip  on;
    gzip_comp_level 1;
    gzip_proxied any;
    gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml%2Brss text/javascript;

    log_format main '$remote_addr - $remote_user [$time_local] '
        '"$request" $status  $body_bytes_sent "$http_referer" '
        '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx_access.log main;

    error_log  /var/log/nginx_error.log crit;

    include /usr/local/nginx/sites-enabled/*;
}

Now we need to set some default fastcgi options in /usr/local/nginx/conf/fastgci_params. I just appended these to the end of the file.

fastcgi_connect_timeout 60;  
fastcgi_send_timeout 180;  
fastcgi_read_timeout 180;  
fastcgi_buffer_size 128k;  
fastcgi_buffers 4 256k;  
fastcgi_busy_buffers_size 256k;  
fastcgi_temp_file_write_size 256k;  
fastcgi_intercept_errors on;  

Next, I like to create a "/usr/local/nginx/sites-enabled" directory that houses each website’s configuration details. First I’ll show the configuration I used to handle URL rewrites for WordPress (this blog). I called the file /usr/local/nginx/sites-enabled/geeksharp.com.

server {  
    listen 80;
    server_name *.geeksharp.com *.geeksharp.info *.geeksharp.org
        geeksharp.info geeksharp.org;
    rewrite ^(.*) http://geeksharp.com$1 permanent;
}

server {  
    listen 80;
    server_name geeksharp.com;
    location / {
        root   /var/www;  # absolute path to your WordPress installation
        index  index.php index.html index.htm;

        # this serves static files that exist without running other rewrite tests
        if (-f $request_filename) {
            expires 30d;
            break;
        }

        # this sends all non-existing file or directory requests to index.php
        if (!-e $request_filename) {
            rewrite ^(.%2B)$ /index.php?q=$1 last;
        }
    }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /var/www$fastcgi_script_name;
        include fastcgi_params;
    }
}

Here’s how I’d set up a rails site. In this example, the file is called /usr/local/nginx/sites-enabled/geeksharp.net.

server {  
    listen 80;
    server_name *.geeksharp.net;
    rewrite ^(.*) http://geeksharp.net$1 permanent;
}

server {  
    listen 80;
    server_name geeksharp.net;
    root /home/scott/geeksharp.net/public;
    passenger_enabled on;
    access_log /home/scott/geeksharp.net-access.log;
    error_log /home/scott/geeksharp.net-error.log;
}

Step 5: Startup scripts

Finally, I like to ensure that all my stuff comes up when the system reboots, so here’s a neato Nginx init.d script that I found. Place this file at /etc/init.d/nginx, and make sure you give it execute permissions (chmod %2Bx!)

#! /bin/sh

### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the nginx web server
# Description:       starts nginx using start-stop-daemon
### END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin  
DAEMON=/usr/local/sbin/nginx  
NAME=nginx  
DESC=nginx

test -x $DAEMON || exit 0

# Include nginx defaults if available
if [ -f /etc/default/nginx ] ; then  
    . /etc/default/nginx
fi

set -e

case "$1" in  
    start)
        echo -n "Starting $DESC: "
        start-stop-daemon --start --quiet --pidfile /usr/local/nginx/logs/$NAME.pid \
        --exec $DAEMON -- $DAEMON_OPTS
        echo "$NAME."
        ;;
    stop)
        echo -n "Stopping $DESC: "
        start-stop-daemon --stop --quiet --pidfile /usr/local/nginx/logs/$NAME.pid \
        --exec $DAEMON
        echo "$NAME."
        ;;
    restart|force-reload)
        echo -n "Restarting $DESC: "
        start-stop-daemon --stop --quiet --pidfile \
        /usr/local/nginx/logs/$NAME.pid --exec $DAEMON
        sleep 1
        start-stop-daemon --start --quiet --pidfile \
        /usr/local/nginx/logs/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS
        echo "$NAME."
        ;;
    reload)
        echo -n "Reloading $DESC configuration: "
        start-stop-daemon --stop --signal HUP --quiet --pidfile /usr/local/nginx/logs/$NAME.pid \
        --exec $DAEMON
        echo "$NAME."
        ;;
    *)
        N=/etc/init.d/$NAME
        echo "Usage: $N {start|stop|restart|force-reload}" >&2
        exit 1
        ;;
esac

exit 0  

Finally, make sure you update your rc.d

sudo update-rc.d nginx defaults  
sudo update-rc.d php-fpm defaults  

Please note that if for some reason nginx won’t start, chances are you have a config issue. When you issue a restart command, the config errors usually will not be displayed. The best way to track them down is to is to simply issue a stop command followed by a start command, and your errors will be displayed. If you ever need to restart your services manually, you can use these commands (I find that restart can be buggy, which is why I explicity stop and start each one):

sudo /etc/init.d/php-fpm stop && sudo /etc/init.d/php-fpm start  
sudo /etc/init.d/nginx stop && sudo /etc/init.d/nginx start  

So that’s it! I know this is an epic post, but I hope it helps someone. Leave comments if you have specific questions or if I left out something particularly useful. Again, I understand that there’s probably a better way to do something, so feel free to share your thoughts! As a final thought, I would like to thank Joshua Dorkin for providing a great article on which I based a lot of this material!