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…)
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.
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
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
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
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.
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"
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!
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? 🙂
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!
cryptk says:
too many nested comments, lol… Hopefully the next one will be coming withing a few days if I have time.
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.
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 […]
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 […]
Guenstige Suchmaschi says:
This is a great Plugin, thank you for this. I will test it on my Blog, hope it will work fine.
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!
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.
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.
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!