Lightning-fast WordPress with PHP-FPM and nginx

Managed servers are slow. They run old versions of PHP on ancient copies of Apache, and loathe the Digg effect (or any similar sudden influx of traffic). In this tutorial, I’ll show how to build a server capable of withstanding a front-page Digg placement, step by step. This will mean your business stays online when it’s most important—when everyone is looking.

We’ll go through the process of building a super-fast, bulletproof custom web server for WordPress. The technology stack we’ll use is Ubuntu, nginx, PHP-FPM, and MySQL. In a future article we’ll look at adding memcached to the mix to take performance even further.

Why VPS?

VPS stands for virtual private server. Basically, you receive a piece of a big, expensive machine for a low monthly price. You pay for a guaranteed amount of RAM, and have access to a certain amount of CPU power.

This can be a much better deal than managed hosting once you have a few websites up and running. However, you have to manage everything yourself, and take care of your server when something goes wrong.

Why nginx?

Nginx is a small, lightweight web server and reverse proxy. It runs on 5.2% of the top one million web servers. In particular, nginx is well-suited to a high traffic site. Its lightweight nature, compared to Apache, means an nginx server can run in a much smaller memory footprint. This makes it the web server of choice for people looking to squeeze the most performance out of a VPS solution.

Why PHP-FPM?

Included with version 5.3.3 of PHP (released in July of this year) is a new FastCGI manager called PHP-FPM. PHP-FPM is a daemon that spawns processes to manage your online applications. So, rather than have your web server running plugins to display and process your PHP code, your PHP code is now run natively, by PHP-FPM.

For our example WordPress installation, we’ll set up an nginx server to serve our static files. When a user requests a PHP page, the nginx server will forward the request to PHP itself. PHP-FPM runs its own server, waiting for users to request their pages.

This separation of features means you can see some incredible speed gains.

Why MySQL?

Basically, because there’s no other choice. At the moment, WordPress only supports MySQL. I always use memcached to lighten the load on MySQL. As I mentioned, I’ll be covering the use of memcached in a future post.

Putting It All Together

Since all this software is relatively cutting edge, we’re going to go ahead and build nearly everything from source. This means you’ll need to have build-essential or an equivalent package installed on your system. I’ll assume you have basic familiarity with Linux and SSH.

If you’re stuck developing in Windows and want to give this setup a go, I’d recommend installing an Ubuntu server inside the free VirtualBox virtualization application.

Step One: Installing nginx

I recommend downloading and installing nginx from source, as the version in most Linux distributions’ package managers are older than we’d like. Nginx is actively developed, and we might as well take advantage of the developers’ hard work.

We’ll start by getting the dependencies; then we’ll grab nginx and build it (check the downloads page for the latest version available):

sudo apt-get install libpcre3 libpcre3-dev libpcrecpp0 libssl-dev zlib1g-dev
cd ~/downloads
wget http://nginx.org/download/nginx-0.8.53.tar.gz
tar zxvf nginx-0.8.53.tar.gz
cd nginx-0.8.53

Now, before we run through configure, we need to set a few preferences. Specifically, where nginx should be installed to. We’ll also set up nginx to use the SSL module, so https works for our server:

./configure --pid-path=/var/run/nginx.pid --sbin-path=/usr/local/sbin --with-http_ssl_module

Finally, we’ll do a make, and a make install:

make
sudo make install

We’ll make a few quick edits to the nginx configuration file, which is located at /usr/local/nginx/conf/nginx.conf. At the top of that file you’ll see these two lines:

# user nobody;
worker_processes 1;

Uncomment the user line and change nobody to www-data www-data, then change worker_processes to 2 instead of 1. Have a look at the rest of the file; there are a number of settings for logs and other options, a sample server declaration, and a few commented-out examples. We’ll be coming back to this file later, but for now you can save it and close it.

nginx doesn’t come with an init script that we can run when the system boots, but there are plenty of good ones available online. Let’s grab one and set it up:


cd ~/downloads
wget http://nginx-init-ubuntu.googlecode.com/files/nginx-init-ubuntu_v2.0.0-RC2.tar.bz2
tar xfv nginx-init-ubuntu_v2.0.0-RC2.tar.bz2
sudo mv nginx /etc/init.d/nginx 
sudo update-rc.d -f nginx defaults

Nginx will now start when your system starts, and you can control it with these commands:


sudo /etc/init.d/nginx stop
sudo /etc/init.d/nginx start
sudo /etc/init.d/nginx restart

That’s it! At this point you should be able to start up nginx, hit http://localhost/ in your browser, and see the default “Welcome to nginx!” page. Next, we’ll install PHP 5.3.3, then finally, configure everything.

Step Two: Installing PHP 5.3.3

Now we’ll install PHP from source. Feel free to change any of the ./configure parameters as you require—the important ones for our purposes are the --enable-fpm and --with-fpm lines:


sudo apt-get install autoconf2.13 libbz2-dev libevent-dev libxml2-dev libcurl4-openssl-dev libjpeg-dev libpng-dev libxpm-dev libfreetype6-dev libt1-dev libmcrypt-dev libmysqlclient-dev libxslt-dev mysql-common mysql-client mysql-server
cd ~/downloads
wget http://us3.php.net/get/php-5.3.3.tar.gz/from/us.php.net/mirror/
tar zxvf php-5.3.3.tar.gz
cd php-5.3.3
./buildconf --force
./configure 
	--prefix=/opt/php5 
	--with-config-file-path=/opt/php5/etc 
	--with-curl 
	--with-pear 
	--with-gd 
	--with-jpeg-dir 
	--with-png-dir 
	--with-zlib 
	--with-xpm-dir 
	--with-freetype-dir 
	--with-t1lib 
	--with-mcrypt 
	--with-mhash 
	--with-mysql 
	--with-mysqli 
	--with-pdo-mysql 
	--with-openssl 
	--with-xmlrpc 
	--with-xsl 
	--with-bz2 
	--with-gettext 
	--with-fpm-user=www-data 
	--with-fpm-group=www-data 
	--enable-fpm 
	--enable-exif 
	--enable-wddx 
	--enable-zip 
	--enable-bcmath 
	--enable-calendar 
	--enable-ftp 
	--enable-mbstring 
	--enable-soap 
	--enable-sockets 
	--enable-sqlite-utf8 
	--enable-shmop 
	--enable-dba 
	--enable-sysvmsg 
	--enable-sysvsem 
	--enable-sysvshm
 
make
sudo make install

Next, we’ll configure PHP by copying over the default php.ini and php-fpm.conf files, and setting up PHP-FPM to run when the system boots:

sudo mkdir /var/log/php-fpm
sudo chown -R www-data:www-data /var/log/php-fpm
sudo cp -f php.ini-production /opt/php5/etc/php.ini
sudo chmod 644 /opt/php5/etc/php.ini
sudo cp /opt/php5/etc/php-fpm.conf.default /opt/php5/etc/php-fpm.conf
sudo cp -f sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm
sudo chmod 755 /etc/init.d/php-fpm
sudo update-rc.d -f php-fpm defaults

Nice! Now PHP-FPM will be running as a daemon at startup.

If you get this or similar when trying to start PHP-FPM:

Starting pfp-fpm ................................ failed.

Make sure your /etc/init.d/php-fpm file has the right path to PHP-FPM, and also ensure you touch the .pid file so that the process can run.

sudo touch /var/run/php-fpm.pid

Success! We now have a running nginx server, and a running PHP-FPM server. All we have to do is combine the two in our site configuration.

Step Three: Setting Up Our WordPress Site

Next, we’ll download WordPress, and install it in our localhost directory for testing. I’m assuming we already have our MySQL database set up with a user specifically for our installation; MySQL is set up exactly the same way, whether you’re using nginx and PHP-FPM or the conventional Apache and PHP stack.

First, we’ll set up the directories necessary for our localhost installation:

mkdir ~/public_html
mkdir ~/public_html/localhost/
mkdir -p ~/public_html/localhost/{public,private,logs,backup}
cd ~/downloads
wget http://wordpress.org/latest.zip
unzip latest.zip
mv wordpress/* ~/public_html/localhost/public
cd ~/public_html/localhost/public
vim wp-config-sample.php

Enter your MySQL configuration into wp-config-sample.php. Then, save the file, and rename it to wp-config.php. You now have only one step left! Making nginx and PHP-FPM recognize the new WordPress server.

Step Four: Configuring nginx for PHP-FPM

We’ll set nginx up to work with Debian/Ubuntu’s normal sites-available and sites-enabled folders for site-by-site configuration. You create site configuration files in sites-available, and then symlink those to sites-enabled to activate them. Let’s first create those two folders:

sudo mkdir /usr/local/nginx/sites-available
sudo mkdir /usr/local/nginx/sites-enabled

You now need to tell nginx to load all the configuration files inside sites-enabled. To do this, edit your /usr/local/nginx/conf/nginx.conf file again. Somewhere inside of the http { ... } block, add the line:

include   /usr/local/nginx/sites-enabled/*;

Then, cut out the entire server { ... } block in nginx.conf—that’s a default server configuration that we’ll be replacing with our virtual hosts in sites-available.

Now for the good part. We’ll create a virtual host for our WordPress site, in sites-available. Create a file called localhost (or wordpress, or whatever) inside your sites-available directory. Here’s what to put in it (replace “username” with your username):

server { 
  listen 80; 
  server_name localhost; 
	 
  access_log /home/username/public_html/localhost/logs/access.log; 
  error_log /home/username/public_html/localhost/logs/error.log; 

  location / { 
    root /home/username/public_html/localhost/public; 
    index index.php index.html index.htm; 

    if (-f $request_filename) { 
      expires 30d; 
      break; 
    } 

    if (!-e $request_filename) { 
      rewrite ^(.+)$ /index.php?q=$1 last; 
    } 
  } 
  
  location ~ .php$ { 
    fastcgi_pass   localhost:9000;  # port where FastCGI processes were spawned 
    fastcgi_index  index.php; 
    fastcgi_param  SCRIPT_FILENAME    /home/username/public_html/localhost/public/$fastcgi_script_name;  # same path as above 
    fastcgi_param PATH_INFO               $fastcgi_script_name;
    include /usr/local/nginx/conf/fastcgi_params;
  } 
} 

The configuration is relatively straightforward: first up, we’re defining the web root directory, and specifying which index files the server should look for. Then we set a 30-day expires header on static files, and redirect any other requests to index.php in our WordPress directory. There are far too many available configuration settings for nginx to cover them all here, but there’s a full list on the nginx wiki.

Now all that’s left is to add your new site to sites-enabled. We do this with a link:

ln -s /usr/local/nginx/sites-available/localhost /usr/local/nginx/sites-enabled/localhost
sudo /etc/init.d/nginx restart

Open up a browser and go to http://locahost/ (or wherever your server is located). You should see the WordPress installation page. Install your WordPress as you usually do. When setting up clean URLs in the WordPress admin, you’ll need to remove index.php from the permalink structure. (For instance, instead of “example.com/index.php/2010/10/10/my-post/” you’d have “example.com/2010/10/10/my-post/”.)

And that’s it! Now you have one lean, mean server for your WordPress site. In the next post, I’ll be looking at a few extra configuration tweaks to our nginx setup, and we’ll also be adding memcached to the mix. Stay tuned!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • MetalCat

    “You page[KS; this right?]“, KS it’s likely “You pay”.

    Though yes FPM and nginx is a good way to go.

    Though it’s not really nginx’s small footprint that is the main advantage, it’s that it’s an event-driven (asynchronous) architecture. so it’s not blocking while waiting for slower resource like databases or network calls.

    Check out “C10K problem” and node.js etc.

    Nice write up.

    • Louis Simoneau

      Ack, ctrl-f FAIL. All tidied up now. You never saw anything.

  • Martin Fjordvald

    Glad to see sitepoint is giving Nginx some love, it’s very well deserved. Sadly there are a few issues with your nginx configuration and a few things I feel could have been pointed out to help the user.

    First of all I’m very happy to see that you use the latest stable version, many repositories provide a version as old as 0.6.x which is full of bugs and security issues, so kudos!

    I do feel you should mention that a user doesn’t necessarily have to use your configure settings, Nginx provides a fairly comprehensive and description list of configure options if you do.

    ./configure –help

    That way you can tailor Nginx to your needs.

    Now the part I feel is a bit of a let down is the Nginx configuration. I’ve written a guide to introduce people to the overall Nginx configuration setup here: http://blog.martinfjordvald.com/2010/07/nginx-primer/ and it’s well worth a read!

    To sum it up a bit you have directives in location blocks which you don’t need to! This leads to directive duplication and in worst case scenarios to path duplications, this means if you ever change things you’ll have to change your paths in multiple places, a clear violation of DRY.

    You can see this in your blog post, you have your root specified in the root location and in your PHP location. If you move the root directive in your server block you can use the $document_root variable in your PHP location and avoid having it duplicated.

    Further, you have
    if (!-e $request_filename) {
    rewrite ^(.+)$ /index.php?q=$1 last;
    }

    There are two problems here, first of all if in location is buggy, this is documented here: http://wiki.nginx.org/IfIsEvil
    It may work or it may not, it’s usually best to avoid it. Thankfully Nginx provides configuration directives meant just for this! Try_files will try the file paths you specify and if none of them work will go to a fall back.

    This means you can set it up like so:

    location / {
    try_files $uri $uri/ /index.php?q=$request_uri;
    }

    First it will try the file path itself, then it will check if it’s a directory and then finally it will send the request to your front controller. This also avoids the rewrite with the unnecessary capture.

    I would also recommend using an explicit static file location instead of if (-f $request_filename) as you might not want all files to expire in 30 days.

    Read my guide and you will save yourself a headache later on when you have to change things.

    • burningion

      Wow, comments like yours are why I love the internet. There’s always an opportunity to learn something more. You’re completely right, and this is a much better approach. Thanks for taking the time out to share, and let’s see if we can’t get the configuration looking better in the article.

  • JR

    An excellent article, for those techy enough to implement it. Two points, however, that I think were glossed over:

    1. If you’ve never been responsible for managing, securing, and maintaining a remote server, then a VPS is simply not an option. Putting sites on a box that you don’t know how to manage or secure is a horrible idea, if you even manage to get them set up to begin with. It should have been made much more clear that running a VPS requires being far more techy than the average “advanced” user, and it isn’t something you pick up over the weekend – I’m a Linux professional, and have been for several years, and I still worry about my servers. Anybody thinking “Oh, I’m pretty good with computers, I’ll be fine” is in for a nasty, nasty surprise. VPS users need to be at least familiar with servers, if not a full-blown sysadmin, or need to employ one, which isn’t cheap.

    2. Having tried running several WordPress sites on a VPS, I can say without question that it is not less expensive than running them on shared hosting. A webhost like HostGator will let you run a (theoretically) unlimited number of sites for $15/mo – and they’ll run quickly. I have VPSs at VPSLink, which is Spry’s unmanaged VPS division, one of the cheapest providers out there. A VPS capable of running one WordPress site – slowly – starts at $35/mo. (I know because I tried.) One capable of running one site reasonably quickly, or a couple at a crawl, jumps to $69.95/mo. There used to be a ~$100 plan, which might have run four or five sites reasonably fast, but now it requires going to another provider. If you’re planning on running quite a few, expect your costs to well exceed $100/mo. Likewise, disk space, bandwidth, and resource allocation will always be less on a VPS than shared hosting – VPS providers have to expect you’ll use 100% of what you’re allowed, where shared hosts know most customers will never use even a fraction of the resources they’re allocated.

    In the end, it’s a numbers game: A shared webhost always has more resources than a VPS customer. They have many, many specialized servers: a cluster of dedicated webservers that run Apache and Squid, a cluster of database servers that run MySQL, a cluster of mailservers, DNS servers, etc. They have access to more bandwidth and disk space, so they can offer more to their shared hosting customers. And, they have a staff of qualified technical support professionals – not just first-level support, but highly qualified sysadmins, database administrators, networking professionals – who provide 24/7 support for their systems. A VPS customer has one small server, which must handle all of the above, and at the same time run all the backend services that run on any server. A thousand specialized servers and a full staff of professionals (for $15/mo) can’t be replaced by a single quasi-server with one part-time (if that) combination sysadmin/DB admin/networking pro (for several times $15/mo).

    In short, unless you’re a very advanced user and are running so many sites that you are going to slow down the operation of a major webhost, then a VPS is not going to be a lower-cost solution. By all means, it may be a more appropriate solution for certain things, and I won’t dispute that for a second, but the cost will be higher, both what is payed out, and in time lost to server maintenance.

    • burningion

      JR,

      I appreciate your response, and you have a lot of valid points.

      Yes, running your own server is more difficult than setting up a managed host. However, as they say, ignorance is bliss.

      I’ve run managed web hosting for years, because it seemed as though it was the cheapest. And indeed it was, until my managed host got hacked.

      And that’s where the cost effectiveness ended. Apparently, there had been a major hack at my “managed” web host, and every site got hacked. This meant loss of income, which destroyed any “cheapness” or “easiness”. Tracking down what got hacked, and what wasn’t was a massive job.

      Also, VPS hosting is a lot cheaper than you say. Slicehost and Linode are just two very cheap VPS hosts. They’ll set you up a server with about 256 megs of RAM for about $20 / mo. In my opinion, if you’re running a business, the investment in effort to manage and protect your own product is always worth it.

    • http://www.callonclick.com/http://www.anseltaft.com/http://www.amphibiacam.com/ open4biz

      As someone who bought into the whole host-on-a-cloud idea, I completely disagree.

      I run a self-managed 20% server slice over at LayeredTech.com. Besides needing to increase the memory to 768k, I’ve encountered very few issues that weren’t self-induced. Such as the time I updated the kernel, not knowing that the latest one wasn’t compatible with their virtualization software. Oops.

      But I learned. I did a fair amount of reading on linux. I asked more than my fair share of questions in some forums. I kept an Evernote cheat sheet of the commands I run the most. I learned how to install software, tweak Apache, rebuild PHP, harden SSH, set up a linux firewall, etc.

      The server now handles more than 20 WordPress sites, serves about 100,000 page views a month, and for me, is proof of concept that linux is becoming more layman friendly. (Well, I do have a background in Windows desktop support and networking, so the concepts weren’t alien.)

      So I don’t think it’s fair to try and scare people from self-managing. All they need is a predisposition to learn and some understanding of servers. It’s not like you were born knowing how to sysadmin yourself. This isn’t rocket science… (which can be learned as well).

      Ansel

  • roosevelt

    Digg effect is long dead, it’s all Reddit Era now :)))

  • A web developer

    Having spent the last 7 years working with Apache, I can tell you that the performance criticism it receives these days (when compared to nginx) is completely unfounded.

    Most of the issues come from the fact that the default “prefork” MPM settings (how many processes to use to serve requests – prefork is 1 request/1 process) are usually not realistic for the given server resources. Fix this, or switch to the worker MPM (which is 1 request/1 thread), and 80% of the performance issues go away.

    Then lower the KeepAliveTimeout value to 1 or 2 seconds.

    Then remove the kitchen-sink loading of modules you are not using.

    And all of the sudden you have a high performance server.
    http://www.devside.net/articles/apache-performance-tuning

    • burningion

      You’re right, most people don’t give Apache enough of a chance.

      I’m currently working on an Apache installation that will be dealing with over 500,000 pageviews a day. And you know what? Apache seems to be doing fine. Once you strip away the bloat and the poor default settings, you can start to see some performance under there. But you’ll also need the hardware to get that performance.

      So can a correctly configured Apache keep up with a correctly configured nginx? I’ll take your settings and build a few realistic tests. And then I’ll post the results here. We’ll see just how big a gap there really is, if any.

      Thanks for the comment and the post, I look forward to some decent Apache settings in the future!

    • http://www.callonclick.com/http://www.anseltaft.com/http://www.amphibiacam.com/ open4biz

      Nice article. Thanks! :)

  • Sastro

    I just installed this this tutorial and it working perfectly.
    Then installed APC for php is double perfectly.
    Now i’m waiting for memcached thing….

  • mike503

    VPS’s won’t scale probably -that- crazy. I’d worry about that.

    PHP-FPM is available as of PHP 5.2.x it has just been officially included in PHP core as of 5.3.3. See http://php-fpm.org/ for pre-5.3.3 versions.

    About the nginx configuration:

    location ~ .php$ {
    fastcgi_pass localhost:9000; # port where FastCGI processes were spawned
    - fastcgi_index index.php;
    - fastcgi_param SCRIPT_FILENAME /home/username/public_html/localhost/public/$fastcgi_script_name; # same path as above
    - fastcgi_param PATH_INFO $fastcgi_script_name;
    include /usr/local/nginx/conf/fastcgi_params;
    }

    Can be rewritten:

    location ~ .php$ {
    fastcgi_pass localhost:9000; # port where FastCGI processes were spawned
    include /usr/local/nginx/conf/fastcgi_params;
    }

    You can put:

    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    in your /usr/local/nginx/conf/fastcgi_params

    and use the try_files mentioned above.

    So, final config would look more like this:

    server {
    listen 80;
    server_name localhost;
    access_log /home/username/public_html/localhost/logs/access.log;
    error_log /home/username/public_html/localhost/logs/error.log;
    index index.php index.html index.htm;
    root /home/username/public_html/localhost/public;
    # if you have to, but i’d go with a different location matching route for expires headers
    if (-f $request_filename) {
    expires 30d;
    break;
    }
    location ~ .php$ {
    fastcgi_pass localhost:9000;
    include /usr/local/nginx/conf/fastcgi_params;
    }
    }

  • http://www.satya-weblog.com/ satya prakash

    Thanks for the article. It gives good insight to all those who are not using ngnix and others

  • http://www.manyse.com sastro

    Please help. my server configuration does not work

    server{
    listen 80;
    server_name http://www.lowtraffic.net;
    access_log /var/log/nginx/lowtraffic.net.access_log;
    error_log /var/log/nginx/lowtraffic.net.error_log;

    root /home/lowtraffic/public_html/;

    location / {
    set $memcached_key $uri;
    memcached_pass 127.0.0.1:11211;
    default_type text/html;
    error_page 404 405 = @fallback;
    }

    location ~* .(gif|jpg|jpeg|png|css|js|ico)$ {
    expires 30d;
    access_log off;
    }

    location @fallback {
    rewrite ^ /index.php?q=$uri last;
    }

    location ~ .php$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /home/lowtraffic/public_html/$fastcgi_script_name;
    include fastcgi_params;
    }
    }

  • David

    This is a really grat article, thanks.

    David.

    http://www.webbgear.co.uk

  • Yann Lossouarn

    I’m currently following your procedure, and wanted to tell that on my setup I needed to add libevent-dev to the prerequisite libraries for php.

    • Jace

      Same here!

    • Louis Simoneau

      Entirely correct. The article originally included instructions for installing memcached, which included installing libevent-dev, so when we made the decision to push that into another post we missed the missing dependency. Thanks for pointing it out, updated in the post now.

  • Jonathan Roy

    Maybe you covered it in a newer article, but you can also use MariaDB as a drop-in replacement for MySQL with WordPress.

  • http://onsman.com/ ronsman

    Thanks, Emilio, that’s fixed.

  • Boyi Shafie

    I’ll have to say, You’ve helped me alot. I constantly got 403 error. At last, I follow your instructions, and No More Headache. TQ Very Much.

  • Pothi Kalimuthu

    Great guide. I have modified it to work with Amazon Linux (@Amazon EC2). For those work with Amazon Linux have a step lesser to do, because PHP and php-fpm can be installed directly using yum.