Skip to content

A short how-to for switching from Apache to nginx

Note: this post is outdated. Use at your own risk.

I am running my sites on a VPS from Slicehost and have had a very good experience. When I started, I set everything up using Apache 2, since that is what I am most familiar and adept with using. Apache works well, but likes more memory than I have in my server. This caused me to use my swap far too much.

I worked with the Apache configuration, finally coming up with the changes below for my /etc/apache2/apache2.conf file, which minimized the swapping, but wasn’t quite enough for me to be happy. Also, if more than one or two people were browsing the site at a time, it forced everything to go much more slowly, because it now got backed up in a queue instead of being served quickly (this was by design, to save memory and prevent swapping). That was not acceptable. Here you can see the changes I had made to get it to run without swapping all the time in a lean-memory environment. I have removed everything but the necessary changes I had made, just to save space here. Apache is so well documented, you should be able to figure the file out as needed using readily available info, if you need to.

Timeout 30
KeepAlive On
MaxMemFree 262144
MaxKeepAliveRequests 2
KeepAliveTimeout 1
<IfModule mpm_prefork_module>
StartServers 2
MinSpareServers 1
MaxSpareServers 3
MaxClients 4
MaxRequestsPerChild 5
</IfModule>
<IfModule mpm_worker_module>
StartServers 2
MaxClients 5
MinSpareThreads 2
MaxSpareThreads 5
ThreadsPerChild 2
MaxRequestsPerChild 3
</IfModule>
HostnameLookups Off

I did some reading and decided to try nginx as a replacement for httpd. The Slicehost folks also have some great how-tos (not only on nginx) and helpful people in their forums, and my friend, Ryan, has a helpful post as well.

There are lots of how-tos around for installing from scratch on a server. Most are good. I am focusing on migrating from Apache2 to nginx. For that, I will assume you have your site set up and running on Apache, your DNS is set properly and pointing to the server, etc.

First, make sure your system is up to date, especially with all security updates. Then, install nginx, either from source or from your Linux distribution’s package repositories. I chose the latter. You also want to install the cgi version of php5. On my Ubuntu 8.10 server, I did this:

sudo aptitude update && sudo aptitude safe-upgrade

followed by:

sudo aptitude install nginx php5-cgi

I made a couple of modifications to my /etc/nginx/nginx.conf file. Here is what I have. My server has two dual core processors, hence the 4 worker processes. I lowered the keep-alive timeout, added the gzip config, and made a couple more simple changes.

user www-data;
worker_processes 4;

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

access_log /var/log/nginx/access.log;

sendfile on;
tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 3;
tcp_nodelay off;

gzip on;
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;

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

After that, I used the php-fastcgi script I found here and put it in /etc/init.d/php-fastcgi.

#! /bin/sh
### BEGIN INIT INFO
# Provides:          php-fastcgi
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start and stop php-cgi in external FASTCGI mode
# Description:       Start and stop php-cgi in external FASTCGI mode
### END INIT INFO

# Author: Kurt Zankl <kz@xon.uni.cc>

# Do NOT "set -e"

PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="php-cgi in external FASTCGI mode"
NAME=php-fastcgi
DAEMON=/usr/bin/php-cgi
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

# If the daemon is not enabled, give the user a warning and then exit,
# unless we are stopping the daemon
if [ "$START" != "yes" -a "$1" != "stop" ]; then
log_warning_msg "To enable $NAME, edit /etc/default/$NAME and set START=yes"
exit 0
fi

# Process configuration
export PHP_FCGI_CHILDREN PHP_FCGI_MAX_REQUESTS
DAEMON_ARGS="-q -b $FCGI_HOST:$FCGI_PORT"

do_start()
{
# Return
#   0 if daemon has been started
#   1 if daemon was already running
#   2 if daemon could not be started
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null || return 1
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --background --make-pidfile --chuid $EXEC_AS_USER --startas $DAEMON -- $DAEMON_ARGS || return 2
}

do_stop()
{
# Return
#   0 if daemon has been stopped
#   1 if daemon was already stopped
#   2 if daemon could not be stopped
#   other if a failure occurred
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE > /dev/null # --name $DAEMON
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently.  A last resort is to
# sleep for some time.
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
[ "$?" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
}

case "$1" in
start)
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
do_start
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
restart|force-reload)
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
exit 3
;;
esac

Then, I set the ownership and permissions appropriately.

sudo chmod u+x /etc/init.d/php-fastcgi

and

sudo chown 0.0 /etc/init.d/php-fastcgi

and set it to run at boot

sudo update-rc.d php-fastcgi defaults 21 23

Most how-to articles tell you here to create the directories to hold your website(s). Since I am doing this on a site that was already running Apache, my sites already existed in /home/myusername/public_html/sitename/public. Note where yours is, because you will need that info.

Ngnix works similarly to Apache for multiple domains. With each you can set up more than one domain on an IP address using virtual domains. If you only have one domain, the setup is easier, but outside the scope of this article. Apache2 puts the sites in directories in /etc/apache2/sites-available for setup, with symbolic links to there for active sites from /etc/apache2/sites-enabled. Nginx can do the same, using /etc/nginx/sites-available and /etc/nginx/sites-enabled.

Make a virtual host (vhost) file for each domain/site you plan to use in /etc/nginx/sites-available. Here’s a sample, based on this domain, but with sensitive info changed:

/etc/nginx/sites-available/matthewhelmke.net

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

server {
listen  80;
server_name  matthewhelmke.net;

access_log  /home/myusername/public_html/matthewhelmke.net/log/access.log;
error_log   /home/myusername/public_html/matthewhelmke.net/log/error.log;

location / {
root   /home/myusername/public_html/matthewhelmke.net/public;
index  index.html index.htm index.php;

# 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 ^(.+)$ /index.php?q=$1 last;
}

}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ \.php$ {
fastcgi_pass   127.0.0.1:9000;
fastcgi_index  index.php;
fastcgi_param  SCRIPT_FILENAME  /home/myusername/public_html/matthewhelmke.net/public$fastcgi_script_name;
include fastcgi_params;
}
}

The first server section rewrites all requests for the domain that use www in front to the same url without the www. The second server section is where all the fun happens. Both are listening on port 80, the standard for http. The second tells nginx where to put the log files (in a special log directory I created for each domain, in each domain’s directory in my /home directory).

The location section is especially important. It tells where to find the root directory for my site, in my case, in a special directory called public. Then, I have two special rewrites. These allow you to use permalinks aka clean urls aka pretty urls aka SEO urls with your site. Once these lines are in the vhost file, you will discover that both WordPress and Drupal allow you to adjust the settings in their admin panels to use them as you would like. I haven’t tried it with other types of content management systems, but I think it would probably work. Without these lines, you have to use the standard url formats.

The other incredibly important bit is that last section. It tells nginx to send all php files to the fastcgi script to be interpreted. Without this, you won’t get php to work.

Once you get your vhost file set up, create a link to it from the sites-enabled directory so that nginx will know to use it.

cd /etc/nginx/sites-enabled/ && sudo ln -s ../sites-available/yourdomain.com

Then, to test it out, start the fast-cgi script, turn off Apache, and start nginx.

/etc/init.d/php-fastcgi start

/etc/init.d/apache2 stop

/etc/init.d/nginx start

If everything is set correctly, you should now be able to view your site using nginx. If it isn’t, you can always stop nginx and start apache2 again to keep your site up while you try to figure out the problem. Once it is up, tested thoroughly, and you are happy, you can remove Apache if you like.

Note: I had some trouble with the fast-cgi script at first, having copied one that had special characters put in it. That happens sometimes when you post code with WordPress. I’m trying to prevent it here by using code tags, but they reset any time there is a blank line, so you may need to cut/paste in sections. If you have problems, and all you did was cut & paste, look at this first as the likely cause.

6 Comments

  1. Joey

    Great write up. Can you compare the site performance now versus the Apache performance?

  2. Thanks. I don’t have objective numbers, but the site is considerably faster as a user, and I haven’t had any swap issues since I made the switch yesterday. I’ll be monitoring for the next several days to confirm none crop up. So far performance is obviously faster and lighter, even if I only have subjective measurements.

  3. Joey

    I have recently started at slicehost (from a shared host) and also see good performance on my (small) wordpress nginx sites. I do not have objective measures either but so far so good.

    I am running hardy and rebooted today because I upgraded due to some ssl security issues (I knew because I run Ubuntu on my laptop).

    I seem to be getting some memory creep but always end up with 80 mb plus spare.

    I have tried throwing Siege at the site and it really seems to hold up without any ill effects.

  4. parp-o

    I’ve been searching for the correct nginx rewrite directives for use with Drupal for a few days now. Tried loads of them, these were the ones that worked straight away.

    Thanks!

  5. Cool! I’m glad this worked for you.

Comments are closed.