Menu

Skip to content
CryptkCoding

CryptkCoding

Ramblings of a Linux administrator

Running WordPress with nginx, php-fpm, apc and varnish

Posted on August 21, 2011 by cryptk

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";
}
acl purge {
        "localhost";
}

sub vcl_recv {
        if (req.request == "PURGE") {
                if (!client.ip ~ purge) {
                        error 405 "Not allowed.";
                }
                return(lookup);
        }
        if (req.url ~ "^/$") {
               unset req.http.cookie;
        }
}
sub vcl_hit {
        if (req.request == "PURGE") {
                set obj.ttl = 0s;
                error 200 "Purged.";
        }
}
sub vcl_miss {
        if (req.request == "PURGE") {
                error 404 "Not in cache.";
        }
        if (!(req.url ~ "wp-(login|admin)")) {
                unset req.http.cookie;
        }
        if (req.url ~ "^/[^?]+.(jpeg|jpg|png|gif|ico|js|css|txt|gz|zip|lzma|bz2|tgz|tbz|html|htm)(\?.|)$") {
                unset req.http.cookie;
                set req.url = regsub(req.url, "\?.$", "");
        }
        if (req.url ~ "^/$") {
                unset req.http.cookie;
        }
}
sub vcl_fetch {
        if (req.url ~ "^/$") {
                unset beresp.http.set-cookie;
        }
        if (!(req.url ~ "wp-(login|admin)")) {
                unset beresp.http.set-cookie;
        }
}

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:

root@temp:~# 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…

(As a side note, this blog does not currently run on this config, but it will be moving to it very VERY soon…)

Posted in Linux Nas-Admin | 18 Comments

18 thoughts on “<span>Running WordPress with nginx, php-fpm, apc and varnish</span>”

  1. 8/24/11
    6:44 pm
    August 24, 2011
    6:44 pm

    Daniel says:

    Brilliant! Thank you so much for this. I have spent many hours researching the best way to do this, and here it all is in one place. I have a couple of comments and questions.

    I am testing this on devel.whatsthatbug.com before I roll it out on <a href="http://www.whatsthatbug.com” target=”_blank”>www.whatsthatbug.com.

    For varnish, you have:

    if (req.http.host ~ “^somesite.cryptkcoding.com$”) {

    Should that be devel.whatsthatbug.com for me?

    The difference between devel.whatsthatbug.com at http://www.webpagetest.org/result/110825_DV_1DKC8… and <a href="http://www.whatsthatbug.com” target=”_blank”>www.whatsthatbug.com at http://www.webpagetest.org/result/110825_QM_1DK9M… is amazing. But it looks like I still have some room for improvement.

    http://www.webpagetest.org/result/110825_B6_1DKBZ… shows the details. Any suggestions on how to turn cookies off for my images in nginx? They are off for some but not all, which seems strange.

    Again, thank you for this! I think I’m ready to go live with it tomorrow.

    • 8/24/11
      7:48 pm
      August 24, 2011
      7:48 pm

      cryptk says:

      Heh, You caught me there… That part of the varnish config shouldn't have actually been in the article. That was another URL being served by that server that I specifically didn't want varnish to cache.

      I removed that snippet from the varnish config on the post and it isn't needed for your setup (unless you have some URL's that you specifically do NOT want cached.) Nothing in the varnish config for wordpress is URL sensitive… and actually if you had that section in there with your devel URL, then varnish wouldn't have been doing anything…

      Thanks for pointing out my mistake! The varnish config as it sits now is what you want.

      -Chris

  2. 8/25/11
    9:03 am
    August 25, 2011
    9:03 am

    Daniel says:

    I am having a conceptual problem. It seems like varnish should be on port 80 and nginx should be on port 8080 if varnish is supposed to sit between nginx and my users. Otherwise, how are people hitting varnish at all? And where am I telling varnish what to cache? The reason I'm asking is, if I look at `varnishtop -i rxurl` for example, there are zero entries.

    Thanks,

    Daniel

  3. 8/25/11
    9:31 am
    August 25, 2011
    9:31 am

    Daniel says:

    Yep, here is what I have in the main varnish config file (I am only running it on one IP address):

    DAEMON_OPTS="-a 199.167.132.56:80

    -T localhost:6082

    -f /etc/varnish/default.vcl

    -u varnish -g varnish

    -S /etc/varnish/secret

    -s file,/var/lib/varnish/varnish_storage.bin,1G"

    And in default.vcl:

    backend default {

    .host = "199.167.132.56";

    .port = "8080";

    }

    And in nginx.conf:

    server {

    listen 199.167.132.56:8080;

    The site loads fast now, and varnish is being utilized. You have it backwards. Let me know how much faster things get for you when you switch it around.

    Of course, I never would have got anywhere near this far without your otherwise excellent work! Thanks again.

    Daniel

    • 8/25/11
      12:56 pm
      August 25, 2011
      12:56 pm

      cryptk says:

      Nope, I cover all of that. First I walk through getting nginx up on port 80. Then I cover adding in varnish with varnish on 80 and nginx on 8080.

      Glad you got everything working though! This really is a great config!

      I will likely do a followup later on how to add mediawiki into this config.

      • 8/25/11
        3:13 pm
        August 25, 2011
        3:13 pm

        Daniel says:

        D'oh!

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

        I missed that line. Sorry.

        I tweaked my varnish config after doing some more googling. It made another measurable difference in performance. I'm averaging about 40-50 client requests per second according to varnishstat and pages are loading a full second faster than they were without these changes. Try it and see what you think:

        DAEMON_OPTS="-a YOUR.IP.ADDRESS.HERE:80

        -T localhost:6082

        -f /etc/varnish/default.vcl

        -u varnish -g varnish

        -p thread_pool_min=200

        -p thread_pool_max=2000

        -S /etc/varnish/secret

        -s malloc,1G"

        • 8/25/11
          4:42 pm
          August 25, 2011
          4:42 pm

          cryptk says:

          Yep, tweaks like those are planned for a second post on advanced varnish tuning which will include performance tweaks like that as well as a good way to use varnish on a server that is running multiple vhosts when each one may need a separate config (for instance if you are running wordpress and mediawiki on the same server).

          I am really glad this article helped you out!

          • 8/25/11
            4:49 pm
            August 25, 2011
            4:49 pm

            Daniel says:

            Awesome. For my next project I wanted to move some more sites that are on the same server into this configuration. I look forward to your guidance! Any idea when you'll be writing that one? 🙂

    • 8/25/11
      12:56 pm
      August 25, 2011
      12:56 pm

      cryptk says:

      Almost forgot to mention. This blog runs on requests… let me know if there is anything in particular you want a post on and I will write it up!

    • 8/25/11
      4:50 pm
      August 25, 2011
      4:50 pm

      cryptk says:

      too many nested comments, lol… Hopefully the next one will be coming withing a few days if I have time.

  4. 8/26/11
    6:07 am
    August 26, 2011
    6:07 am

    Daniel says:

    Have you had any issues with the varnish and/or nginx cache not being purged properly when something changes in WordPress? I've got the ip address that varnish is running on listed in w3tc and I have "Enable varnish cache purging" checked. However, when there is a new post or a change to a post, it is not being reflected. Even if I 'empty all caches' in w3tc, the data is still stale.

  5. 9/5/11
    12:29 pm
    September 5, 2011
    12:29 pm

    Jeff Beard » Blog Archive » Lighting a fire under Wordpress - blog.blog says:

    […] details so I did the research myself and came up with a number of articles with the best being this article on setting up nginx, PHP-FPM, APC, memcached and the W3 Total Cache WordPress plugin. The […]

  6. 9/11/11
    8:11 am
    September 11, 2011
    8:11 am

    Tecnología y negocios » De LAMP a LEMP says:

    […] deseran hacer lo mismo les recomiendo este howto de CryptkCoding que sintetiza varios otros que he visto en la red y funciona para Ubuntu, aunque con […]

  7. 3/19/12
    2:00 am
    March 19, 2012
    2:00 am

    Guenstige Suchmaschi says:

    This is a great Plugin, thank you for this. I will test it on my Blog, hope it will work fine.

  8. 5/18/12
    8:00 pm
    May 18, 2012
    8:00 pm

    Damien says:

    Hey cryptik thanks for writing this article this is the first time I've been able to setup a LEMP stack successfully. I had one question though, I have to sites setup on nginx one being just a 301 redirect but the other one is a wordpress install. If I have the ip.address:8080 in the nginx config file the sites do not work but if it's port 80 they work. I've tried rebooting the server, restarting nginx, php5-fpm and varnish. Any help will greatly be appreciated!

    • 5/22/12
      6:33 am
      May 22, 2012
      6:33 am

      cryptk says:

      seems like you have varnish mis-configured. Make sure that nginx is listening on port 8080, and varnish is listening on port 80, connecting to it's backend on port 8080.

  9. 12/26/12
    5:55 pm
    December 26, 2012
    5:55 pm

    www.wishpot.com says:

    Aw, this was an exceptionally nice post. Taking the

    time and actual effort to create a good article… but what

    can I say… I hesitate a whole lot and never manage to get nearly anything

    done.

  10. 9/12/13
    4:30 am
    September 12, 2013
    4:30 am

    web design singapore says:

    Hey cryptik thanks for composing this content this is initially I've been able to create a LEMP collection efficiently. I had one query though, I have to websites installation on nginx one being just a 301 divert but the other one is a wordpress platforms set up. If I have the ip.address:8080 in the nginx config computer file the websites do not perform but if it's slot 80 they perform. I've tried restarting the server, restoring nginx, php5-fpm and varnish. Any help will significantly be appreciated!

Comments are closed.

Pages

  • Who Am I?

Blogroll

  • Failverse
  • major.io
  • SyntheticWorks

Archives

  • January 2015
  • February 2013
  • September 2012
  • August 2012
  • April 2012
  • March 2012
  • September 2011
  • August 2011
  • April 2011
Proudly powered by WordPress
Theme: Flint by Star Verte LLC
 

Loading Comments...