Install WordPress with nginx, php-fpm, apc and varnish on ubuntu.

Recently I built a server to host the blogs and other PHP powered websites of a few family members.  I wanted something lightweight, efficient and fast.  With that in mind I threw out the “standard” of Apache and it’s mod_php and instead went with something else entirely.  This article is going to be geared at people running a server with Ubuntu 10.10 or newer (sorry LTS fans… php5-fpm isn’t available in your repos… but you can backport it fairly easily).  I’m going to be including some config file examples as well, everything you need to get this up and running will be included… and it’s easier than you think 😉  Catch the details after the break

First we are going to start off with a vanilla Ubuntu 10.10 server (this works on 11.04 the same as well, and probably will on 11.10 as well once it is released).  Install all of the updates available in the repo and give it a reboot if any of them require it.  Now that we have a nice up-to-date platform to work on we are going to start by installing nginx and php-fpm… super easy

aptitude install nginx php5-fpm

Now unlike apache with it’s mod_php, nginx doesn’t have a “built in” way to serve PHP content.  This is why we are using php-fpm to run php in fastcgi mode.  We have two options on how to have the two talk, we can either have them communicate over a TCP socket with an IP address and a port or we can use a unix socket.  TCP sockets are great if things are running on different servers and need to talk to one another but in this case, they are both running on the same server so we are going to adjust the php5-fpm config to use a unix socket instead.  Edit the config file located at /etc/php5/fpm/pool.d/www.conf and find the below line

listen = 127.0.0.1:9000

and change it to…

;listen = 127.0.0.1:9000
listen = /var/run/php5-fpm.sock

Now we have php-fpm listening on a unix socket, lets configure nginx.  Go to the directory /etc/nginx/conf.d and make a file named php5-fpm.conf and put this in there:

upstream php5-fpm-sock {
server unix:/var/run/php5-fpm.sock;
}

and then go to /etc/nginx/sites-available and make a file named WordPress.  Below is it’s contents, I am assuming that you are going to install WordPress into /var/www/wordpress but if you are putting it somewhere else then adjust the document root paths (there are two spots).  Also be sure to substitute the required values for the parts that are in ALL CAPS (except for SCRIPT_FILENAME… leave that one alone, it is supposed to be all caps):

server {
    listen       80;                # your server's public IP address
    server_name  SOMEURL.com;                   # your domain name
    root         /var/www/wordpress/;  # absolute path to your WordPress installation

    index index.php;

    access_log /var/log/nginx/SOMEURL.com-access_log;
    error_log /var/log/nginx/SOMEURL.com-error_log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ .php$ {
        try_files $uri =404;
        fastcgi_index index.php;
        fastcgi_pass php5-fpm-sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include /etc/nginx/fastcgi_params;
    }
}

Now we will enable this config by symlinking it into the sites-available folder.  The below command should do the trick:

ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/010-wordpress

If you don’t have a URL then be sure to remove the default configs symlink so that WordPress will be served over your IP address

rm /etc/nginx/sites-enabled/000-default

Those two files work together… the first one establishes an upstream that points to the unix socket that we configured php-fpm to listen on and the second actually serves the content and routes php files to that upstream to be rendered.  Now we need to get WordPress installed, the below commands will do the trick:

mkdir -p /var/www
cd /var/www
wget http://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz

Ok, now we have nginx installed and php-fpm installed and the WordPress files are in place…  There are a few dependencies for WordPress, lets get them installed

aptitude install mysql-server php5-mysql

The installer will prompt you for a MySQL root user password…it can be anything you like (normal password strength rules are not enforced, but should be followed anyway)

Now we need to create a database for WordPress to use, log into MySQL with the following command

mysql -u root -p

Give it your MySQL root password that you entered during the install and then run the following commands at the mysql> prompt

CREATE DATABASE `wordpress`;
GRANT ALL PRIVILEGES ON `wordpress`.* TO 'wordpress'@'localhost' IDENTIFIED BY 'SOMEREALLYSTRONGPASSWORD';
FLUSH PRIVILEGES;
EXIT

Please don’t literally use SOMEREALLYSTRONGPASSWORD as your password… come up with something good… xkcd rules apply

Now we need to restart php-fpm and nginx to get the config changes we made earlier pulled in

service nginx restart
service php5-fpm stop
service php5-fpm start

I did a stop and start on php5-fpm because there is a small gaff in it’s control script that sometimes causes it to not restart properly with the restart command… it’s easily fixable by putting a “sleep 1″ in the right spot in it’s init.d script though.

If you have funky firewall rules, make sure that port 80 is open to the outside world

Guess what… As far as the server side of the WordPress install go’s… YOU’RE DONE!  You already have a WordPress install that is much faster than running it through Apache and mod_php.  Go to thr URL you have configured for it (or the IP address if you don’t have one) and go through the WordPress installer  The only things you need to know for the installer is that your database name is wordpress, the database username is wordpress and your password is SOMEREALLYSTRONGPASSWORD (but hopefully your password is something good)

Now that we have WordPress installed and running… lets make some magic happen with some nice caching and make this thing more digg proof.  First install the following packages

aptitude install php-apc varnish

APC gets a 30MB cache by default which is fine for a small blog, you should adjust it though if you are running a larger site or if you are running a lot of plugins.  If you need to adjust it, edit the file at /etc/php5/fpm/conf.d/apc.ini and add the following line to the bottom (substituting the number of MB for the cache size to be in place of 100)

apc.shm_size=100

You will need to stop and start php5-fpm after making this change for it to take effect.  That’s it for APC… it’s super simple… on to varnish.

First we need to enable varnish, do this by editing the file at /etc/default/varnish and find the below line

START=no

and change it to… you guessed it…

START=yes

Next we need to adjust the nginx config that we did earlier.  Just change the port number on the Listen line to 8080

then restart varnish and nginx

service varnish restart
service nginx restart

The way varnish works is that it sits between your web server and your users and it caches things in order to be able to serve them to the users faster because the web server doesn’t have to do any processing… the data is already stored in it’s rendered form.  There is one gotcha with varnish though, it won’t cache anything with cookies attached because it will assume that cookies means highly dynamic and as such should not be cached… How does this affect WordPress… well, WordPress attaches cookies to damn near everything, even things that have no business having cookies attached to them like images and plain text CSS and JavaScript files.  Luckily we can configure varnish to strip off these cookies.  We are going to make a WordPress config for varnish so create a file at /etc/varnish/wordpress.vcl with the following contents

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}
sub vcl_recv {
    /* These rules will apply for all the requests served */
    /* Post requests will not be cached */
    if (req.request == "POST") {
        return (pass);
    }
    /* Normalize encoding/compression */
    if (req.http.Accept-Encoding) {
        if (req.http.Accept-Encoding ~ "gzip") { set req.http.Accept-Encoding = "gzip"; }
        elsif (req.http.Accept-Encoding ~ "deflate") { set req.http.Accept-Encoding = "deflate"; }
        else { remove req.http.Accept-Encoding; /* unknown algorithm, just remove */ }
    }
    
    /* Host: www.mysite.com; these rules will apply only for mysite.com */
    if (req.http.host ~ "^(?:www.)?mysite.com") {
        unset req.http.vary;
        /* Remove the following line if your site is serving content in the language it reads from the Accept-Language header */
        unset req.http.accept-language;
        
        /* If I am logged in to wordpress, I DO NOT WANT TO SEE cached pages */
        if (req.http.cookie ~ "wordpress_logged_in") {
            return (pass);
        } else {    /* If I'm just a regular visitor */
            /* If the request is for pictures, javascript, css, etc */
            if (req.url ~ ".(jpg|jpeg|png|gif|css|js)$") {
                /* Remove the cookie and make the request static */
                unset req.http.cookie;
                return (lookup);
            }
            
            /* Try to lookup in the cache */
            return (lookup);
        }
    }
    
    /* If the host header is empty, just return error */
    error 404 req.http.host;
    return (lookup);
}
sub vcl_fetch {
    /* Host: www.mysite.com */
    if (req.http.host ~ "^(?:www.)?mysite.com") {
        /* Do not cache POST requests */
        if (req.request == "POST") {
            return (pass);
        }
        /* If the request is for pictures, javascript, css, etc */
        if (req.url ~ ".(jpg|jpeg|png|gif|css|js)$") {
            /* Cache it, and make it last 2 hours */
            set beresp.ttl = 7200s;
            /* Make the request static by removing any cookies set by those static files */
            unset beresp.http.set-cookie;
            /* Deliver the cached object */
            return (deliver);
        }
        /* If I am logged in to wordpress, I DO NOT WANT TO SEE cached pages */
        if (req.http.cookie ~ "wordpress_logged_in") {
            return (pass);
        } else {
            /* Cache anything for 2 minutes. When the cache expires it will be cached again and again, at the time of the request */
            set beresp.ttl = 120s;
            return (deliver);
        }
    }
}
Change “mysite.com” to the name of your website.

Now we need to tell varnish to use this config file. Edit /etc/default/varnish again and find the section

DAEMON_OPTS="-a :6081 
             -T localhost:6082 
             -f /etc/varnish/default.vcl 
             -S /etc/varnish/secret 
             -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"

and change it to

DAEMON_OPTS="-a YOUR.IP.ADDRESS.HERE:80 
             -T localhost:6082 
             -f /etc/varnish/wordpress.vcl 
             -S /etc/varnish/secret 
             -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"

There are two changes there, make sure you catch them both… now restart varnish again with

service varnish restart

Guess what… you are DONE! But because the life of a sysadmin is never done… lets install some WordPress plugins to make this thing even faster and look nicer.  Find and install these plugins from within your WordPress Admin panel.

nginx Compatibility
W3 Total Cache

And then back on your server install the following packages

aptitude install memcached php5-memcache

Restart php5-fpm to pull in the new php extension

service php5-fpm stop
service php5-fpm start

Back in the WordPress admin panel, go to the plugins list and make sure that the PHP4 version of nginx Compatibility is disabled and the PHP5 version is enabled.  Now you can set up permalinks and they will work in nginx.

Now go into the W3 Total Cache settings and set the following options in the General section

Page Cache: Enabled (Method: Memcached)
Minify: Enabled (Method: Memcached)
Object Cache: Enabled (Method: Memcached)
Varnish Cache Purging: Enabled (put 127.0.0.1 in the text area)
Browser Cache: Enabled

Then click any of the Save Settings buttons.  Now go to the Minify tab at the top and uncheck the the top box for rewriting the URL structure and then scroll through the minify section and check every checkbox labeled Enable.  Click any of the Save All Settings buttons here and then go back to the General tab.  Lastly at the very top next to where it says Preview, click Deploy and then click Disable.  Now at the top there will be a prompt telling you that settings have changed and it recommends purging the page cache, go ahead and lick that button.

All done!  You now have a supercharged WordPress blog!  Happy Blogging!

Oh yeah! almost forgot… I decided to benchmark it using apache benchmark and I hit it with 10 concurrent connections for 1,000 connections:

This is ApacheBench, Version 2.3
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 50.56.112.115 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests

Server Software:        nginx/0.7.67
Server Hostname:        50.56.112.115
Server Port:            80

Document Path:          /
Document Length:        21320 bytes

Concurrency Level:      10
Time taken for tests:   10.332 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      21682000 bytes
HTML transferred:       21320000 bytes
Requests per second:    96.79 [#/sec] (mean)
Time per request:       103.316 [ms] (mean)
Time per request:       10.332 [ms] (mean, across all concurrent requests)
Transfer rate:          2049.42 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       25   26   0.2     26      29
Processing:    77   78   0.5     77      79
Waiting:       25   26   0.2     26      27
Total:        102  103   0.6    103     106
ERROR: The median and mean for the processing time are more than twice the standard
       deviation apart. These results are NOT reliable.

Percentage of the requests served within a certain time (ms)
  50%    103
  66%    103
  75%    104
  80%    104
  90%    104
  95%    104
  98%    104
  99%    105
 100%    106 (longest request)

106 millisecond longest response with almost 100 requests per second… not too bad but we obviously aren’t stressing it enough… lets do it again but this time with 100 concurrent connections for 10,000 connections!

This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 50.56.112.115 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests

Server Software:        nginx/0.7.67
Server Hostname:        50.56.112.115
Server Port:            80

Document Path:          /
Document Length:        21320 bytes

Concurrency Level:      100
Time taken for tests:   11.306 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      216811617 bytes
HTML transferred:       213200000 bytes
Requests per second:    884.46 [#/sec] (mean)
Time per request:       113.064 [ms] (mean)
Time per request:       1.131 [ms] (mean, across all concurrent requests)
Transfer rate:          18726.64 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       25   26   0.2     26      27
Processing:    77   87  38.0     78     510
Waiting:       25   35  37.9     26     457
Total:        102  112  38.1    103     536

Percentage of the requests served within a certain time (ms)
  50%    103
  66%    104
  75%    104
  80%    104
  90%    107
  95%    157
  98%    256
  99%    310
 100%    536 (longest request)

Longest response of 536 milliseconds but 95% of all connections were served in 157ms or faster!!  Over 800 requests per second!! I was bandwidth capped at this point so I couldn’t benchmark with any higher concurrency numbers, but that’s pretty impressive! And this was running on a server with an amazing 512MB of ram… your cell phone likely has more than that, it never went over 200MB of ram used…

Now you have to remember that ab does not include time for things like downloading images and such so it is a good tool to test how fast you can serve the page source, but not how fast the page actually will load in a browser.  For that I use the free test over at Load Impact.  First I will post the results graph and then I will explain what it means…

One thing to notice here is that as the test progressed and the server was being hit with more concurrent users, the load time stayed just about the same at just a touch over 2 seconds… What this means is that it wasn’t that the server couldn’t send out the content any faster… it means that the Load Impact servers couldn’t download it any faster.  Load times staying the same while concurrency go’s up indicates that the limitation is on the part of the people downloading the content… not the server sending it out.  I was also watching the bandwidth usage on the server itself in real-time as the test ran and it confirmed that… as the concurrency went up the bandwidth usage raised proportionately.  Unfortunately this means that the Load Impact test isn’t as accurate as it could be, but it does still indicate that this configuration is very powerful and very fast… Last but not least, the ram usage data… this was pulled right smack in the middle of the 50 clients section of the Load Impact test:

[email protected]:~# free -m
total       used       free     shared    buffers     cached
Mem:           496        409         86          0          3        167
-/+ buffers/cache:        238        258
Swap:         1023          0       1023

The important line in this is the -/+ buffers/cache line… that states that the server was only actively using 238 MB of ram while Load Impact was hitting it the hardest…  Lets add very lightweight to the powerful and fast that I mentioned earlier…

Leave a Reply