There have been several fellow members of the Habari community that have decided to switch to Nginx lately. Knowing that I run Nginx exclusively (and set up and maintain the instance that hosts the Habari website itself), they’ve asked questions from time to time.
Unfortunately, it turns out some of the guides floating around (even some written and publicized by major hosting companies) are helping users configure things sub-optimally and in some cases outright wrong. The standard set of config adjustments I use have been assembled from dozens of places and lots of time, reading, and experimentation. So I’m going to try and take you through setting everything up, from scratch, the right way.
I’ve used these same basic steps to install PHP-FPM and Nginx on a variety of versions of Ubuntu. The packages and default configs should remain very similar, regardless of your exact version (or even if you’re using Debian).
Before you install anything new on your box you should always make sure you’ve updated the list of available packages and upgraded any that have updates available:
sudo aptitude update sudo aptitude safe-upgrade
Getting PHP installed is the first step. This is ridiculously simple, you’ll just need to install the
sudo aptitude install php5-fpm
You’ll also want to install whichever other PHP packages you need for your specific setup. In my standard production setups, I usually add
php5-cli php5-curl php5-mcrypt php5-mysqlnd php5-sqlite.
Accept any dependencies aptitude wants to install. Once it’s done churning, it’s time to configure your PHP-FPM pool to handle requests.
Configuring Your Pool
This is the only place where we have to do a little math, and perform some guesswork. You see, you have to tell PHP-FPM how many separate processes it should be allowed to spawn at any one time. If you’ve ever configured Apache this will be very familiar (and possibly miserable).
If you set everything too low, you’ll have spare memory sitting on your system not being used and during a spike in traffic clients could end up queueing up endlessly, waiting for their turn for PHP to process their request.
Of course if you set everything too high, you’ll run out of memory during a spike in traffic because PHP-FPM is trying to process too many requests simultaneously and your box will jump over the cliff and kill itself.
How do you get the right mix, then? Well, that’s going to take more than a single config session to figure out. Once you’re done and your box is serving normal levels of traffic you’ll have to keep an eye on the amount of free memory you have available and how many processes php-fpm usually has hanging around and make adjustments. In the meantime, we’ll just go with some pretty safe (low) defaults, since it’s vastly better to have some free resources available than the alternative.
From simple observation, I’ve seen that my php-fpm processes tend to start off small and slowly grow into the 20-25mb range as they serve requests. On my system (with about 512mb of RAM), that means after everything else is running I can easily allow up to 5 php-fpm processes. We’ll go with that for now…
The main PHP-FPM configuration file is
/etc/php5/fpm/php-fpm.conf. For reference here is a copy of the default version I’ve got. We won’t be changing anything in here, but note the last line. Any config files in the
pool.d directory are included — that’s where we’ll stick all of our goodies.
By default there is also a pool pre-configured for you in
pool.d/www.conf (again, here is a copy of the default version I have). As much as I hate editing default files that could change in future versions of the packages they belong to, there’s really no way to avoid it here.
Choose Your Own Adventure
For some peculiar personal reason I like breaking things up into their own files, so I have several different ones in
pool.d. We have to edit the default
www.conf anyway, so feel free to make the changes directly if you like. The only hard advantage I can see in having separate files is that if the default should be replaced by a future package update, you’ll throw errors because configuration values are duplicated across the files. Think of it as a rather harsh safety net of sorts…
All of the
pool.d config files I use can be found in this single Gist, if you want a single page to reference back against. We’ll be going through them individually, too.
First, let’s edit
www.conf and simply comment out (add a ; at the beginning of the line) anything we’re going to override:
listen = 127.0.0.1:9000
pm.max_children = 10
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
Now down to the nitty-gritty individual files.
You can have any number of “pools” you like. They will each maintain their own set of processes and live in their own little sandbox. For most setups there’s no reason to have more than one (especially if you are the only one hosting sites on the server), so we’ll configure a single global pool called “www” — just like the default.
Config files are simply INI files, so anywhere you see “[www]”, that means we’re defining any of the settings that follow as belonging to the “www” pool.
If you ever need to simply make sure that php-fpm is alive and handling requests, there is a special “ping” URL you can visit that will simply return “pong”. This defines what path php-fpm will look for ping requests on, we’ll actually make it work (and limit it to any particular vhosts we like) later when we configure Nginx.
Here’s where we define exactly how many processes php-fpm is allowed to spawn. I’ve added comments above each line explaining what they do.
By default php-fpm listens on a TCP port, just like your web server. This works great if you’ve got a cluster of web servers handing requests back to separate servers which handle PHP requests, but on a single server there’s not much point and a Unix socket can be (minorly) faster.
To go along with our
ping.conf endpoint, we’ll also enable one for some detailed statistics. This might not be of much use to you unless you’ve got some external script scraping it for monitoring or performance graphing, but it will tell you when php-fpm was last started, how many requests it has served, the current number of processes, clients in the queue, etc.
And that’s it. Restart PHP-FPM (
sudo /etc/init.d/php5-fpm restart) and everything should be good to go.
Now it’s time to get Nginx installed and configured to actually process PHP requests!
The version of Nginx that comes with Ubuntu can be incredibly outdated. In the past I have always used the Nginx PPA repository to get more recent versions, but this appears not to be as supported as it was in the past. Fortunately the Nginx team has an official repo you can add easily.
These instructions start on the Nginx wiki, but can use some more user-friendly details.
First off, Nginx signs their packages with their own PGP key. Before
apt will allow this unknown key, you need to import it:
wget -O - http://nginx.org/keys/nginx_signing.key | sudo apt-key add -
Then you need to figure out which release codename you are using so you get the right version of Nginx for your system. This varies between distros, so…
In Ubuntu run
cat /etc/lsb-release. Look for the
In Debian run
cat /etc/os-release. Look for the
VERSION value and your “codename” is in parenthesis after the numeric version.
Now we can finally add the new sources to apt:
sudo vim /etc/apt/sources.list.d/nginx-stable.list
The lines you add vary depending on whether you are using Debian or Ubuntu.
For Ubuntu boxes, add the following two lines, substituting your codename where it says “precise”:
deb http://nginx.org/packages/ubuntu precise nginx deb-src http://nginx.org/packages/ubuntu precise nginx
If you’re running Debian, you’ll need these two lines instead, of course substituting your appropriate codename for “squeeze”:
deb http://nginx.org/packages/debian/ squeeze nginx deb-src http://nginx.org/packages/debian/ squeeze nginx
If you had previously installed an old version of Nginx (such as your distro’s native version) you can get rid of it quickly using the command
sudo aptitude purge nginx nginx-light nginx-full nginx-extras nginx-common. Note that we’re using
purge, which also makes sure all the related config files are gone, just in case.
Now we’re good to go. Note that in the official release package there is only one version, you don’t have an option between nginx-full or nginx-light. Just update your list of packages to pull in the new repos and you’re good to go:
sudo aptitude update sudo aptitude install nginx
Nginx’s default config is much less annoying than PHP-FPM’s — there’s not a single thing we need to change. Because new versions of Nginx may come with new default configurations, you shouldn’t edit the default
Instead, you can stick all of your custom modifications in the
/etc/nginx/conf.d directory, much like we did with the
pool.d directory when we were configuring PHP.
Again I split everything out into its own distinct configuration file, just so it’s easier to keep track of things and find the bits you’re looking for if you need to change something down the line.
The entire goal of this process is to get PHP working with Nginx, right? Well, here we go…
This makes sure that
index.php is included first in the list of files we’ll consider a directory index. More importantly, it sets up a named “upstream” server that we’ll use to hand all requests for PHP files to.
Using the named upstream is very important. You’ll have a bunch of vhosts setup, each of which have to include a location block telling Nginx to hand any requests for PHP files off to PHP-FPM for processing. If you should change your PHP-FPM config (for example, you need to expand and decide to put it on its own server) having the single named upstream server means you only have to update this in one place, rather than in each separate vhost.
Now that we’ve got PHP working, there are still a bunch of other knobs we should turn and settings we should tweak. Let’s run through a bunch of those quickly.
By default, Nginx does not alter the character encoding of anything it serves. Since we should definitely be serving everything we possibly can as UTF-8 these days and files tend to come from lots of different places, the first thing we want to do is make sure any other screwy character sets are converted.
The charset directive will not only cause Nginx to re-encode anything that is not in the defined character set, it will also add it to the
Content-Type HTTP header so browsers know.
By default Nginx does not modify any of the headers sent with your content. For static resources, though, one of the first optimizations you should make for any site is to set a far-future Expires header.
The expires directive will do just that for you.
Most people will try to limit (based on file extension) which files the Expires header gets set on. Personally I’ve never liked this approach - you’re pretty much guaranteed to miss something. A bad analogy would be whitelisting (rather than blacklisting) email providers to avoid SPAM.
So I set the Expires header on… everything. Anything that should not be cached for a long period of time should set its own headers to prevent that. PHP does this on any page in which a session exists automatically (in other words, most pages where you have to log in and content would be dynamic), so most people probably don’t need to worry much.
You can, of course, look up any variety of examples on how to use regex in a location block to limit based on file extension.
The Nginx Gzip module compresses content before transmission to save on bandwidth. On flakey mobile connections this can be especially important.
If you’re going to use SSL, this is a very important one. The default SSL config for Nginx has a number of potential weaknesses.
Most people have probably had to adjust a couple of INI values for PHP to allow larger file uploads. Well, now you’ve got one more thing to adjust. The default Nginx client_max_body_size is 1mb. That’s too small for most everything these days, so I go ahead and up it to a more reasonable (yet still memory-conscious) limit.
Yes, this means that if someone tries to attack your site by constantly uploading multiple simultaneous large files, your system could run out of memory. Let’s be realistic: there are probably dozens of better ways someone who’s set on being malicious could interfere with the normal operation of your site.
Now that we’ve got all that stupid stuff out of the way, it’s time to get a vhost configured so you can actually start seeing your new site in action!
All vhosts go in
/etc/nginx/sites-available. It doesn’t technically matter what you name them, but you should keep a rigid standard. I name all of mine exactly what they are, like
What about an SSL vhost? Well, it’s very similar, so I’ll only point out the differences.
And for the record, I name them very similarly to the non-SSL vhosts, but keep them in separate files so they can be enabled and disabled individually and I can tell at a directory glance which ones support SSL: