I switched! No more Apache for this web site.

I've been wanting to try out nginx for quite some time now. I've heard a lot of good experiences from people I know that use it. And part of my job as a sysadmin is to make suggestions on which technology can be used for each situation, so it's always good to expand my knowledge pool.

What is this about

This post will look like a howto about configuring nginx to serve Drupal 7 sites, but I'll keep a lot of rough edges when the steps are straight forward. If there are any questions, I can answer them in the comments.

I won't cover the installation of Drupal itself since it's basically downloading an archive, extracting it in your RootDirectory and running the install.php page. So I suppose here that you can do that part on your own. Else, the process is fairly well documented out there ;)

Installing

First off, Debian Squeeze holds a version of nginx which is a bit older than the current "legacy" release in its repositories. So, I added the squeeze-backports repository, which hold the 1.0.4 version -- not that bad, since the current stable version of nginx is 1.0.5.

The tricky part here is that nginx doesn't server PHP pages "natively" like Apache does with libapache2-mod-php5. You need to use FastCGI to serve the PHP code (named "backend" from here), and make nginx pass on the requests to this backend.

I chose to use the php-cgi package to provide for the FastCGI backend, since it's really easy to set up.

I've read good things about php-fpm, though: it spawns processes on an "as needed" basis so that you don't have too much of them when you server's doing nothing and you can expand to accomodate more clients when the demand is high. Also, you can run different processes under different user/group and chroot combinations. I also saw that FPM support was added into php 5.3.3, but Debian doesn't build a "php5-fpm" package, yet. When this is available, I'll most probably switch the backend. (There's a package available in the dotdeb repositories, but I didn't want yet another external package source, just for PHP)

So, in order to install everything:

apt-get install nginx php5-cgi php5-mysql php5-gd php5-cli

Setting up the backend

Now, let's make the backend run. You'll need to create your own init script, since none is installed with the php-cgi package.

Here's the script that I took on some page (sorry, I would've attributed, but I lost the page in the history since I browsed so much to gather info). I placed it in /etc/init.d/php-fcgi :

#!/bin/bash

### BEGIN INIT INFO
# Provides:          php5-fcgi
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the php5-cgi daemon, using a socket file
# Description:       starts the php5-cgi daemon, using a socket file
### END INIT INFO

VARDIR=/var/run/php5-cgi
#BIND=127.0.0.1:9000
BIND=$VARDIR/php5.sock
PIDFILE=$VARDIR/php-fastcgi.pid
FASTCGI_USER=www-data
FASTCGI_GROUP=www-data
# 15 of those take up to 11*5.8% of memory (326.65 Mb of RAM!!)
PHP_FCGI_CHILDREN=7
PHP_FCGI_MAX_REQUESTS=500
export PHP_FCGI_CHILDREN
export PHP_FCGI_MAX_REQUESTS

PHP_CGI=/usr/bin/php5-cgi
#DAEMON=/usr/bin/env
#DAEMON_OPTS="- USER=$USER PATH=/usr/bin PHP_FCGI_CHILDREN=$PHP_FCGI_CHILDREN PHP_FCGI_MAX_REQUESTS=$PHP_FCGI_MAX_REQUESTS $PHP_CGI -b $BIND"
#NAME=`basename $PHP_CGI`
DAEMON="/usr/bin/spawn-fcgi"
DAEMON_OPTS="-P $PIDFILE -s $BIND -u $FASTCGI_USER -g $FASTCGI_GROUP -f $PHP_CGI"
NAME="spawn-fcgi"
DESC="PHP FastCGI"

test -x $DAEMON || exit 0

set -e

. /lib/lsb/init-functions

start() {
      echo -n "Starting $DESC: "

      if [ ! -d $VARDIR ]; then
          mkdir $VARDIR
          chown $FASTCGI_USER:$FASTCGI_GROUP $VARDIR
          chmod 0755 $VARDIR
      fi

      #start-stop-daemon --quiet --start --background --chuid "$FASTCGI_USER" --exec $DAEMON -- $DAEMON_OPTS
      start-stop-daemon --quiet --start --chuid "$FASTCGI_USER" --exec $DAEMON -- $DAEMON_OPTS
      echo "$NAME."
}
stop() {
      echo -n "Stopping $DESC: "
      # The controlling process is actually php5-cgi while it's running
      start-stop-daemon --quiet --stop --pidfile $PIDFILE --exec $PHP_CGI || true
      rm $PIDFILE  # Cleanup the pidfile since the above command doesn't
      echo "$NAME."
}

case "$1" in
    start)
      start
  ;;
    stop)
      stop
  ;;
    restart)
      stop
      start
  ;;
    status)
      status_of_proc -p $PIDFILE "$DAEMON" php5-fcgi && exit 0 || exit $?
  ;;
    *)
      echo "Usage: php-fcgi {start|stop|restart|status}"
      exit 1
  ;;
esac

The original author of this script used TCP connections on port 9000 on localhost, as you can see commented out. But I prefered to ask the backend to use a Unix socket to remove the TCP overhead.

I'm also using a pid file instead of relying on killall, and also the directory where the pid file and socket file are is automatically created by the init script so that you don't have to worry about this.

Now, here's something that I picked up on a couple of sites: if you don't set the option 'cgi.fix_pathinfo' in your php.ini file to 0, it is possible to exploit your site by uploading an image, say blah.jpg, and then accessing it with '/path/to/blah.jpg/hahaha.php'. Since the whole thing ends with '.php', nginx sends this to the backend. The backend will then happily interpret the file blah.jpg as PHP code and run it. YUCK. Also, this option defaults to 1.

So, open the file /etc/php5/cgi/php.ini, uncomment the option and change its value to 0:

cgi.fix_pathinfo=0

Ok. The backend should now be able to start if you invoke the script with "start" as an argument. Try that out now.

Now we want to make sure this service is started up upon boot so that if you reboot the machine, it starts back up:

update-rc.d php-fcgi defaults

Configuring nginx

Nearly there: you only need to configure your domain under nginx.

I'll cut this very round: most of the configuration can simply be copied as-is from the nginx wiki. You'll need to change the path to the socket file, though.

But with this config alone, you'll lack an important part: Drupal 7 now has Image fields built into core, and in order to optimize things (I guess.. :\ ), Drupal generates thumbnails and other formats for your image on demand. For example, links to your profile picture point to 'sites/your_site/files/styles/..../some-name.ext'. And the trick here is that this file won't exist until you request it from Drupal (via PHP).

I got that working with the help of this repository holding nginx config examples. In the file 'sites-available/drupal.conf', there's the interesting snippet:

    ## Drupal 7 generated image handling, i.e., imagecache in core. See:
    ## https://drupal.org/node/371374.
    location ~* /files/styles/ {
        access_log off;
        expires 30d;
        try_files $uri /index.php?q=$uri&$args;
    }

This tells nginx to try and get the file from disk first, and if it doesn't exist, try accessing it via index.php. I've changed this snippet a little so that it fits better in my config, but this made image field files work for me.

I've also changed the block from the wiki about '.txt' and '.log' files with a snippet from the config from github to also protect some important files like the ones ending in '.inc' and '.module'.

So, here's my whole config, which I placed under /etc/nginx/sites-available/lelutin.ca :

server {
        server_name lelutin.ca;
        root /var/www/lelutin.ca; ## <-- Your only path reference.

    index index.html index.htm index.php;

    access_log /var/log/nginx/$server_name.access.log;
    error_log /var/log/nginx/lelutin.ca.error.log;  ## hmm $server_name doesn't work here

        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }

        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }

        # This matters if you use drush
        location = /backup {
                deny all;
        }

        # Naughty, naughty Zoot!
        location ~ ^/sites/[^/]+/settings.php$ {
                deny all;
        }

        ## Replicate the Apache <FilesMatch> directive of Drupal standard
        ## .htaccess. Disable access to any code files. Return a 404 to curtail
        ## information disclosure. Hide also the text files.
        location ~* ^(?:.+\.(?:htaccess|make|txt|log|engine|inc|info|install|module|profile|po|sh|.*sql|theme|tpl(?:\.php)?|xtmpl)|code-style\.pl|/Entries.*|/Repository|/Root|/Tag|/Template)$ {
                return 404;
        }

        location ~ \..*/.*\.php$ {
                return 403;
        }

        location / {
                # This is cool because no php is touched for static content
                try_files $uri @rewrite;
        }

        location @rewrite {
                # Some modules enforce no slash (/) at the end of the URL
                # Else this rewrite block wouldn't be needed (GlobalRedirect)
                #rewrite ^/(.*)$ /index.php?q=$1&$args;
        rewrite ^ /index.php last;
        }

        # Use an SSH tunnel to access those pages. They shouldn't be visible to
        # external peeping eyes.
    location = /install.php {
                allow 127.0.0.1;
                deny all;
    }
    location = /update.php {
                allow 127.0.0.1;
                deny all;
    }

        location ~ \.php$ {
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_intercept_errors on;
                fastcgi_pass unix:/var/run/php5-cgi/php5.sock;
        }

    ## Drupal 7 generated image handling, i.e., imagecache in core. See:
    ## https://drupal.org/node/371374
    location ~* /sites/.*/files/styles/ {
        access_log off;
        expires 30d;
        try_files $uri @rewrite;
    }

        # Fighting with ImageCache? This little gem is amazing.
        location ~ ^/sites/.*/files/imagecache/ {
                try_files $uri @rewrite;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
                expires max;
                log_not_found off;
        }
}

Now enable your site by linking it into sites-enabled:

ln -s /etc/nginx/sites-available/lelutin.ca /etc/nginx/sites-enabled/

Reload nginx with '/etc/init.d/nginx reload', and your site should be working!

Conclusion, or the appreciation of the change

This won't be very scientific since I don't have performance graphs yet, and not too much traffic either :P

With Apache, I thought my web site was slow to respond, since my VPS is kinda sluggish. I thought it responded a bit faster than before when I made the switch, but I can't prove it. Also, nginx takes less memory than Apache so I can now tune MySQL to use a bit more RAM and I guess this just can't hurt :)

I finally have some minimal experience with nginx and I thought it was easier to configure than Apache, both because there are less features (so less crud to worry about) and because generally you don't have to fiddle with loading modules yourself. Also, the configuration syntax being "declarative" by design (as opposed to Apache where the order is very important in some cases) can be confusing at first if you come from an Apache environment, but it kind of makes more sense that way when you get used to it.