Lightning-fast WordPress with PHP-FPM and nginx

    Kirk Kaiser
    Share

    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!