A Production Quality WordPress Configuration Tutorial

The blogging software used to power this site, WordPress, has just had its 10th anniversary. In the last decade it has become incredibly popular, and currently accounts for about 18% of all websites. Due to my job at (mt) Media Temple, I’ve gotten to admin several popular blogs at various times over the years, and thus have a pretty good idea of how to optimize everything for good efficiency. In this WordPress configuration tutorial, I’m going to walk you through the production quality setup that I have for this blog, which makes use of Ubuntu Linux, Nginx, php5-fpm and MariaDB.


UPDATE: I have recently switched over from using the WP Super Cache plugin for caching to the W3 Total Cache plugin. My reasons are varied. While I don’t think it provides quite the same level of bare metal performance for page caching that WP Super Cache does, I still think it’s the better choice right now. It has significant additional options, is trivial to configure, and makes great use of the APC cache features if available. If there’s sufficient interest I’ll update this post or make another to talk about it.


I’ll be making several assumptions throughout this article. Specifically:

  • You are using Ubuntu Lucid (10.04) or a newer supported release.
  • You are using a “naked” install of Ubuntu (no control panels or anything of that sort).
  • You have the ability to run all the commands I’ll list as root using sudo.
  • You’re just going to use the server you’re using for a WordPress blog, or you have a good enough understanding of user permissions not to create any security risks.
  • You’ll be serving up a blog at example.com, and you’ll be running various services with user and group permissions of a user called example, which needs to exist on your system.

Step 1: Install Needed Software

There’s a variety of things you’ll want to install on your system. For starters, let’s install Nginx and MariaDB. Nginx is very easy as I have a Launchpad PPA with all the goodies you’ll need ready to go. In case you don’t already have it, install python-software-properties with:

1
sudo apt-get -y install python-software-properties

Now, you can add my Nginx PPA and install it:

1
2
3
sudo add-apt-repository -y ppa:chris-lea/nginx-devel
sudo apt-get update
sudo apt-get -y install nginx-full

MariaDB is also very easy to install. You’ll need to import their signing key first with the command:

1
sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db

When you install the server software, you’re going to have to enter a password for the root MariaDB user (which is NOT to be confused with the system root user, though they are used in similar ways here). Looking ahead, let’s pre-select a strong password with the apg password generator.

1
2
sudo apt-get -y install apg
apg -x 10 -m 10

This will generate a half dozen reasonably strong 10 character passwords to pick from. Choose one and keep it handy. Now, to install MariaDB, use your favorite editor (I’m assuming it’s Emacs, obviously) and create the file:

/etc/apt/sources.list.d/mariadb.list

Put the following* into the file:

1
2
deb http://mirror.jmu.edu/pub/mariadb/repo/5.5/ubuntu lucid main
deb-src http://mirror.jmu.edu/pub/mariadb/repo/5.5/ubuntu lucid main

*If you are using a distribution different than Ubuntu Lucid (10.04), replace lucid in the above two lines with the distro name you have installed.

Now, install MariaDB with the commands:

1
2
sudo apt-get update
sudo apt-get -y install mariadb-server

The installer will prompt you for a password for the root user, so enter the password you got from the apg output earlier. For a pro tip, make a file at ~/.my.cnf with the following contents:

1
2
3
[client]
user = root
password = <YOURPASSFROMAPG>

and change the permissions so only you can read it:

chmod 600 ~/.my.cnf

Now you won’t have to enter the username and password when you type mysql to use the MariaDB client.

The last thing to install is the needed PHP software. If you are running Ubuntu Lucid, use the following command to add a PHP5 PPA maintained by Ondล™ej Surรฝ:

1
sudo add-apt-repository -y ppa:ondrej/php5

If you are using a release newer than Lucid you won’t need to do that step. Next, install all the PHP things we’ll need with:

1
2
3
4
sudo apt-get update
sudo apt-get -y install php-apc php-geshi php-pear php5 php5-cli \
php5-common php5-curl php5-fpm php5-gd php5-imagick php5-mysqlnd \
php5-pspell

Step 2: Configuring the Software

We’ll need to configure the things we’ve just installed for optimal performance. Let’s start with the database. Configuring a database correctly is a career level amount of knowledge these days, so there’s nothing I can really tell you in a post like this that will cover every case. But there are a few things we can do to the default setup that will generally always help in this particular WordPress case.

First, open up the file

/etc/mysql/my.cnf

in your text editor. Look for a line that reads:

1
tmpdir          = /tmp

and add the following three lines underneath it:

2
3
4
skip-external-locking
skip-name-resolve
character-set-server = utf8

These make sure that the default character set is UTF-8, that the server doesn’t try and do DNS based hostname lookups (which it shouldn’t need to do here) and that external locking isn’t enabled. Note that with locking disabled, you’ll need to make sure to shutdown the server if you ever need to run myisamchk to repair something.

Next, you will probably want to increase the default value for the key_buffer. What you use depends on how much free memory you have on your system, but increasing it from the default 16M value is a good idea if you can. This site currently uses the setting:

key_buffer              = 128M

Finally, you may want to change the default value for the database query cache. Look for the line that reads:

query_cache_size        = 16M

The default of 16M is probably fine for most blogs. If you get a lot of traffic you may wish to raise this value, but never raise it to more than 512M as it starts doing more harm than good at that point. Even for a blog with a lot of traffic, 128M or 256M is generally plenty. Restart the server to pick up these changes with

1
sudo /etc/init.d/mysql restart

Of course, you’ll want to create a database for your blog. I’ll assume it’s called example_com_wordpress. You’ll want to generate another password as we did before that’s specific to this database. Do NOT use the same password as you have for the root user. Once you have this, enter the MariaDB command prompt by simply typing

mysql

at the command line. Then, use the following commands to create your database and get it ready for use (note how we’re at the MariaDB prompt):

MariaDB [(none)]> CREATE DATABASE example_com_wordpress;
Query OK, 1 row affected (0.00 sec)
 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON example_com_wordpress.* TO `example`@`localhost` IDENTIFIED BY 'NEWPASSFROMAPG';
Query OK, 0 rows affected (0.04 sec)
 
MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.04 sec)

Then just type exit to get out of MariaDB, and your database is ready to go.

Next, let’s configure php5-fpm. Open up the file at

/etc/php5/fpm/pool.d/www.conf

and look for the user and group directives. Change these so that PHP runs as your example user like this:

user = example
group = example

Then, look for the listen directive and change it to read:

listen = /var/run/php5-fpm.sock

so that Nginx can communicate to PHP via a Unix domain socket and thus bypass the TCP stack which isn’t needed. Finally, there are some directives for the process manager that could use some changes. The ideal configuration depends on your traffic and hardware, but this site is currently using:

pm = dynamic
pm.max_children = 40
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 5
pm.max_requests = 500

and that should be a good starting point for most blogs.

Lastly for PHP, you should make one quick edit to the file:

/etc/php5/fpm/php.ini

where you’ll want to make sure the following is set:

cgi.fix_pathinfo = 1

You should now be able to fire up your PHP processes with the command:

sudo /etc/init.d/php5-fpm start

The last, and most involved thing you’ll need to configure is Nginx. To begin, we’ll also want to run it as our example user. Open up the file:

/etc/nginx/nginx.conf

and set the user directive to

user example;

Generally speaking, you want to have as many worker processes as you do physical CPU cores. If you’re unsure how many cores you have, the command

grep -c 'processor' /proc/cpuinfo

will tell you. So, if you have a four core machine, set

worker_processes 4;

Next, we’ll do a virtual host configuration for our example.com site. If you’re going to run a production WordPress site, it’s generally recommended to use some sort of caching plugin if you expect any real traffic. Our configuration is going to be tailored to take advantage of the WP Super Cache plugin, which works very well and is written by one of the key WordPress engineers.

Create a new file called example.com here:

/etc/nginx/sites-available/example.com

To begin with, let’s define an Nginx upstream for PHP listening to the Unix socket we specified earlier. Put the following into the beginning of our example.com config file:

1
2
3
4
# Upstream to abstract backend connection(s) for php
upstream php {
    server unix:/var/run/php5-fpm.sock;
}

After that comes some fairly straightforward Nginx directives to tell it what port to listen to, what the server name is, where to log, and so on:

5
6
7
8
9
10
11
12
13
14
15
server {
    listen 80;
    server_name example.com;
 
    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log;
 
    add_header X-Frame-Options DENY;
 
    root /var/www/example.com;
    index index.php index.html;

The only thing of possible note here is the add_header directive at line 12, which should keep your site from being embedded in a <frame> or <iframe> anywhere. If for some reason you want that to be possible, remove that line. After this, we have some rewrite directives to make the generated sitemap work correctly:

17
18
    rewrite ^/sitemap_index\.xml$ /index.php?sitemap=1 last;
    rewrite ^/([^/]+?)-sitemap([0-9]+)?\.xml$ /index.php?sitemap=$1&sitemap_n=$2 last;

and then some specific location directives for the favicon, robots.txt file, and to keep people out of some common directories and files that shouldn’t be public:

20
21
22
23
24
25
26
27
28
29
30
31
32
33
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }
 
    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }
 
    location ~ /\.svn|/\.ht|/\.git {
        deny all;
    }

Now on to the good part, where we set Nginx up to use WP Super Cache. It’s easier to explain how this works if you can already see the configuration, so here it is, and we’ll go over it next:

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    location / {
        # This is cool because no php is touched for static content.
        # include the "?$args" part so non-default permalinks doesn't break when using query string
        try_files $uri
                  $uri/
                  /index.php?$args;
 
        gzip on;
        gzip_static on;
        gzip_disable "MSIE [1-6]\.";
        default_type text/html;
        gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
 
        if (-f $request_filename) {
            break;
        }
 
        set $supercache_file '';
        set $supercache_uri $request_uri;
 
        if ($request_method = POST) {
            set $supercache_uri '';
        }
 
        if ($query_string) {
            set $supercache_uri '';
        }
 
        if ($supercache_uri ~ ^(.+)$) {
            set $supercache_file /wp-content/cache/supercache/$http_host$1index-http.html;
        }
 
        if (-f $document_root$supercache_file) {
            rewrite ^ $supercache_file break;
        }
 
        if (!-e $request_filename) {
            rewrite ^ /index.php last;
        }
 
    }

Lines 35 to 46 are fairly obvious. They tell Nginx where to look for an index file with the try_files directive and set up gzip compression. The gzip_static directive is somewhat interesting. It tells Nginx that if it sees a file that is supposed to be gzipped, it should look for a file in the same directory with the same name that ends in “.gz”, and if it’s there, to serve that directly. Doing this keeps the server from having to compress the file every time it is served.

The really interesting part, however, starts on line 48. This is where we get Nginx to play nicely with WP Super Cache. First, if the request corresponds to an actual file on the filesystem, we tell Nginx to just serve it and move on. Next we set some variables we’ll need. If the request type is a POST, or if there’s any sort of query string, we disable caching by setting $supercache_uri to be blank. If we’ve made it this far and that variable isn’t blank, then we look for a deterministically named file at

/wp-content/cache/supercache/<the domain name>/<the request uri>/index-http.html

For example, if you had a standard “About” page located at http://example.com/about/, Nginx would look for the file

/wp-content/cache/supercache/example.com/about/index-http.html

This exactly corresponds to how WP Super Cache writes out its “supercached” files to the filesystem. The beauty of this setup is that if a cached file exists, Nginx will serve it without ever having to talk to PHP whatsoever. This is great, because Nginx is amazingly good at serving up static content such as these cached files, and it keeps PHP from having to do work unless it actually needs to. If there’s any legitimate “secret sauce” in this configuration recipe, this is it.

The last stanzas we need tell Nginx how to talk to PHP using FastCGI, and then just sets some expire headers for our static content types:

77
78
79
80
81
82
83
84
85
86
87
88
89
    location ~ \.php$ {
        #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_intercept_errors on;
        fastcgi_pass php;
    }
 
    location ~* \.(?:js|css|png|jpe?g|gif|ico|html|txt)$ {
        expires 7d;
        log_not_found off;
    }
}

We are now ready to turn Nginx on. We’ll need to remove the default configuration that Nginx ships with, symlink our configuration into the sites-enabled directory, and then fire it up.

cd /etc/nginx/sites-enabled
sudo rm -fv default
sudo ln -s ../sites-available/example.com .
sudo /etc/init.d/nginx start

If that all works, we’re on to the third and final stretch.

Step 3: Installing and Setting Up WordPress

This is a tutorial about server configuration, so I’m going to assume that you can handle the basics of getting and installing WordPress itself. You’ll want to put everything in the directory /var/www/example.com, which you’ll need to create and set to be owned by our example user:

cd /var/www
sudo mkdir example.com
sudo chown -R example:example example.com

Be sure when you put the WordPress files in there, you’re installing them as the example user. Edit the wp-config.php file to use our example_com_wordpress database with the credentials we set up earlier. Also, be sure to set up random values for the unique keys and salts. Automattic has handy endpoint to help you do this.

Once you have WordPress up and running, there’s just a few more steps before you’re ready to start your blogging empire. First, set up pretty permalinks for your blog. In the WordPress admin, just go to Settings->Permalinks, choose the “custom” option, and put the following in for the link structure:

/%year%/%monthnum%/%day%/%postname%/

This will give you the standard date-centric link structure that many popular blogs have.

The final step is to set up WP Super Cache. Go to Plugins->Add New and search for “WP Super Cache”. Since PHP is running as the same user who owns the WordPress files and directory, it can install the plugin for you (it can also easily upgrade WordPress for you, as an added bonus). You’ll then need to configure it. Navigate to the settings page for the plugin, and click on the Advanced tab. Check the box to “Cache hits to this website for quick access”, then click the radio button for “Use mod_rewrite to serve cache files”. You should ignore the warnings at the top of the page that complain about mod_rewrite not being installed. The plugin assumes you’re using Apache to serve the site, but you’re not, so this warning is superfluous. Finally, scroll down and click the “Update Status” button to save your changes.

After this, log out of the admin, navigate to your home page, and reload it a few times. If you view the page source, down near the bottom, you should see some comments that indicate the page is a cached page. If so, congratulations, you’re all done!

In Conclusion

This is a setup I’ve refined over several years, starting from when keeping Techcrunch up and running was part of my job description. It’s worked well for me for a lot of WordPress sites (including this one), so I hope you find it useful. If there are questions or issues, please just let me know in the comments.

22 thoughts on “A Production Quality WordPress Configuration Tutorial

  1. Luc De Brouwerc

    Great post!

    Do you have any other motivation to still use the URL with the date in it other than “[it] will give you the standard date-centric link structure that many popular blogs have.”?

    I know the patch in WordPress 3.3 to make ‘%postname%’ URLs play nicer on system resources still adds one additional query to the stack but I was wondering if you had any other reasons.

    Reply
    1. chris lea Post author

      Yes, you certainly can. The only differences are:

      • You don’t have to add the external PPA for PHP (you can if you want, but you don’t have to).
      • You should reference precise instead of lucid when you put in the .list file for the MariaDB repository.

      Other than that, everything should basically be identical to set up on precise. In fact, I'm hoping to get this blog migrated to precise this week if time permits. Let me know if you run into any issues.

      Reply
    1. chris lea Post author

      The above was all set up on a VPS, actually. There’s no real hard and fast rules for how much RAM you want, outside of “more is generally better”. It really depends a lot on what plugins you’re using and how you need to set up MariaDB / MySQL, since that will be using up most of your memory for a high traffic site.

      All that said, with the above setup, you can really serve quite a lot with as little as 1G of RAM. 2G would be plenty for the vast majority of popular blogs.

      Reply
  2. shawn

    Have you considered extending nginx with pagespeed and fastcgi_cache? (The later allows you to use a plugin for purging cache on new posts/edits/comments/etc and seems to be faster than varnish)

    I’ve had a very hard time finding a good ppa with the most recent versions of pagespeed and fastcgi + a few other good ones to use as using unbuntu it has to be compiled.

    Also, do you have any further suggestions on optimizing everything as this tutorial pretty much leaves most everything at default levels and I’m sure with your background you have much more ‘up your sleeves’ ๐Ÿ™‚

    Reply
    1. chris lea Post author

      I haven’t looked into trying to do the caching with fastcgi_cache because it seems like it would be doing effectively the same thing as what ends up happening with wp-super-cache, and with wp-super-cache I can just trust that they’ve figured out everything they need to for cache invalidation.

      As for ngx_pagespeed, the problem there is that the standard way to install it requires linking to PSOL binary libraries that they provide. Launchpad won’t let you do this because the Debian packaging requirements don’t allow it. You can’t build the PSOL libraries from source either as the make command always returns an error (even though it succeeds) and this will break the build process on Launchpad.

      As for further optimizations, I’ve put everything in there that I know if that I think is basically universally applicable. Most of the further optimizations would be database (and thus workload) dependent and would probably be closely tied to things like which specific plugins a person is using. If I think of anything additional that seems to apply globally I’ll probably do another post.

      Hope this helps!

      Reply
  3. Toby

    Great post.. Been looking for something like this to get a really solid WP install process in place, thank you. I’m curious what kind of maintenance you do, and how regularly, and your preferred methods for backing up data.

    Reply
  4. Oliver Nielsen

    Would like to read more about how you configure W3 Total Cache.

    In particular whether you think database caching or object caching is needed, and what it’s for. When to use those, and when not to.

    Have a nice weekend!
    Oliver

    Reply
    1. chris lea Post author

      The short and almost always accurate answer is “always use object caching if you can”. The only reason to use database caching is if for some reason, typically because of a restraint of your hosting environment, you can’t use object caching.

      If you can, use object caching.

      Reply
  5. Toby

    One minor point – in the FCGI setup in the Nginx conf, it shows a comment stating:

    #NOTE: You should have “cgi.fix_pathinfo = 0;” in php.ini

    However, earlier on the instructions state that this setting should be set to 1. Which is correct?

    Thanks!

    Reply
  6. Pingback: エーモン工業 1168 圧着接続端子 【配線コードの接続に】:クレールオンラ

  7. Pingback: moltenモルテン 各種スポーツホイッスル ドルフィンプロ wdfpbk★【P27Mar15】:KYOE

  8. Pingback: 【楽天市場】アペンドキャップケース(ピンク/ベリーピンク):カメラLIFE応

  9. Pingback: 【お買い得】象印(ZOJIRUSHI) 圧力IH炊飯ジャー 「極め炊き 豪熱羽釜」(5.5合) NP-BS10-RA

  10. Pingback: お米10kgまでなら同梱OK★【安価良品】神戸茶房 烏龍茶 1ケース(500mlペット*24

  11. Pingback: 英国製?イギリス製?スコットランド製 GREEN GROVE タータンチェックマフラー Antique Dress S

  12. Pingback: バイオチャレンジ 業務用パック 即効型(Bタイプ) 10リットル(5リットル

  13. Pingback: 【送料無料】?クリアランスSALE?ストール ショール 花柄 起毛 レディー

  14. Pingback: パナソニックLEDダイニング用ペンダントライト LGB15206LE1(電球色):ソフマッ

  15. Pingback: H4uh.com ืœื™ืœื“ื™ื - ืื™ืจื•ืขื™ื,ืžืชื ื•ืช,ืžืฆื•ื•ื”,ื™ื•ื ื”ื•ืœื“ืช,ื—ืชื•ื ื”,ืœื—ืชื•ื ื”,ืื˜ืจืงืฆื™ื•ืช,ืœืื™ืจื•ืขื™ื,ื ื™ืฉื•ืื™ืŸ,ื‘ืจ ืžืฆื•ื•ื”,ืกื“ื ืื•ืช,ืœื‘ืจ ืžืฆื•ื•ื”,ื”ื—ืชื•ื ื”,ื‘ืช ืžืฆื•ื•ื”,ืืจื•ืขื™ื,ืจื•ื•ืงื•ืช,

Leave a Reply