High-performance PHP on apache httpd 2.4.x using mod_proxy_fcgi and php-fpm.
With the release of apache httpd 2.4 upon an unsuspecting populace, we have gained some very neat functionality regarding apache and php: the ability to run PHP as a fastCGI process server, and address that fastCGI server directly from within apache, via a dedicated proxy module (mod_proxy_fcgi.)
- starting from release 5.3.3 in early 2010, PHP has merged the php-fpm fastCGI process manager into its codebase, and it is now (as of 5.4.1) quite stable.
php-fpm was previously found at http://php-fpm.org/
This means that we can now run secure, fast, and dependable PHP code using only the stock apache httpd and php.net releases; no more messing around with suphp or suexec - or, indeed, mod_php.
- installing software packages
- editing configuration files
- controlling service daemons.
From release 5.3.3 onwards, PHP now includes the fastCGI process manager (php-fpm) in the stock source code.
Your distribution or OS will either include it in the stock PHP package, or make it available as an add-on package; you can build it from source by adding --enable-fpm to your ./configure options.
This provides us with a new binary, called php-fpm, and a default configuration file called php-fpm.conf is installed in /etc.
The defaults in this file should be okay to get you started, but be aware that your distribution may have altered it, or changed its location.
Inside this configuration file you can create an arbitrary number of fastcgi "pools" which are defined by the IP and port they listen on, just like apache virtualhosts.
The most important setting in each pool is the TCP socket (IP and port) or unix domain socket (UDS) php-fpm will be listening on to receive fastCGI requests; this is configured using the listen option.
The default pool, [www], has this configured as listen 127.0.0.1:9000: it will only respond to requests on the local loopback network interface (localhost), on TCP port 9000.
Also of interest are the per-pool user and group options, which allow you to run that specific fpm pool under the given uid and gid; goodbye suphp!
Let's just use the defaults as shipped and start the php-fpm daemon; if your distro uses the provided init script, run
Or if not, start it manually with
php-fpm -y /path/to/php-fpm.conf -c /path/to/custom/php.ini
If you don't provide php-fpm with its own php.ini file, the global php.ini will be used. Remember this when you want to include more or less extensions than the CLI or CGI binaries use, or need to alter some other values there.
You can include per-pool php.ini values in the same way you would define these in apache previously for mod_php, using php_[admin_](flag|value).
See the official PHP documentation for fpm for all possible configuration options.
I also changed the php-fpm.conf logging option so I can easily see what is being logged specifically by php-fpm:
If you don't set a php-fpm logfile, errors will be logged as defined in php.ini.
Side note: you can force a running php-fpm to reload its configuration by sending it a SIGUSR2 signal.; SIGUSR1 will cycle the log files (perfect for a logrotate script!). A little experimentation goes a long way
That's php-fpm taken care of; if there were no errors during startup it should be listening and ready for connections.
apache httpd 2.4
- editing httpd.conf
- understanding the vhost context
- understanding URL-to-filesystem namespace mapping
- controlling the apache httpd daemon
This release of apache httpd has introduced two noteworthy features: a new proxy module specifically for fastCGI (mod_proxy_fcgi), and the move to the event MPM as the default apache process manager.
As with the worker MPM of the previous version, the threaded model of this MPM causes issues when mod_php is used with non-thread-safe third-party PHP extensions.
This has been a bane of mod_php users ever since apache 2.2 was released, practically forcing them to cobble together fastcgi solutions, or use the much slower and memory-hungry prefork MPM.
To work the magic with the PHP fastCGI process manager, we will be using a new module, mod_proxy_fcgi, which is intended specifically for communicating with (possibly external) fastCGI servers.
Make sure you include the proxy_fcgi module in your httpd.conf so we can use its features; since this requires the base proxy module, ensure both are loaded (uncommented):
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
Now, there are different ways to actually forward requests for .php files to this module, ranging from everything (using ProxyPass) to very specific or rewritten files or patterns (using mod_rewrite with the [P] flag).
The method I chose (using ProxyPassMatch) lies somewhere in between these in complexity and flexibility, since it allows you to set one rule for all PHP content of a specific vhost, but will only proxy .php files (or URLs that contain the text .php somewhere in the request).
TCP socket (IP and port) approach
Edit the configuration for a vhost of your choice, and add the following line to it:
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/path/to/your/documentroot/$1
DirectoryIndex /index.php index.php
Look confusing ? Let's run through it:
- only proxy content that matches the specified regex pattern; in this case:
from the documentroot onwards, match everything ending in .php (with the dot escaped), optionally followed by a slash and any continued path you like (some applications use this so-called PathInfo to pass arguments to the php script.)
The ^ (caret) and $ (dollar) signs are used to anchor both the absolute start and end of the URL, to make sure no characters from the request escape our pattern match.
The nested parentheses enable us to refer to the entire request-URI (minus the leading slash) as $1, while still keeping the trailing pathinfo optional.
forward via mod_proxy_fcgi, using the fastCGI protocol, to the port our php-fpm daemon is listening on.
This determines which fastcgi pool will serve requests proxied by this rule.
IMPORTANT! This must exactly match the real filesystem location of your php files, because that is where the php-fpm daemon will look for them.
php-fpm just interprets the php files passed to it; it is not a web server, nor does it understand your web servers' namespace, virtualhost layout, or aliases.
IMPORTANT! Read the above again
expands to the entire request-URI from the original request, minus the leading slash (because we already added that above.)
- DirectoryIndex /index.php index.php
- a request for / will need to be mapped to a resource on the fcgi backend. Failure to address this may cause a blank response, commonly known as a WSOD (White Screen of Death), especially if only a request URI containing the php extension is proxied, such as this example. The processing chain will first map a request for / to /index.php or any other index.php file relative to the current request uri, then proxy to the PHP-FPM backend correctly.
unix domain socket (UDS) approach
Edit the configuration for a vhost of your choice, and add the following line to it:
ProxyPassMatch ^/(.*\.php(/.*)?)$ unix:/path/to/socket.sock|fcgi://127.0.0.1:9000/path/to/your/documentroot/
- the path to your fpm socket
Note that with this approach, the captured request URI ($1) is not passed after the path
Proxy via handler
With this approach, you can check for the existence of the resource prior to proxying to the php-fpm backend.
# Defining a worker will improve performance
# And in this case, re-use the worker (dependent on support from the fcgi application)
# If you have enough idle workers, this would only improve the performance marginally
<Proxy "fcgi://localhost:9000/" enablereuse=on max=10>
# Pick one of the following approaches
# Use the standard TCP socket
# If your version of httpd is 2.4.9 or newer (or has the back-ported feature), you can use the unix domain socket
For the impatient
Very simple example
If you're interested into the proof of concept and want to leave the tweaking for later, you can use the following recipe. It'll conjure up the standard php info page listing all compiled-in and loaded extensions, and all runtime configuration options and script info.
First, create a file, /var/www/info.php containing:
<?php phpinfo() ?>
The assumption is that /var/www is the DocumentRoot of an existing vhost.
Inside this vhost, add the following line:
ProxyPassMatch ^/info$ fcgi://127.0.0.1:9000/var/www/info.php
Reload apache with apachectl graceful and you can now call up the phpinfo page using http://example.com/info
This is a very simple example, mapping one unique URL to a single PHP file.
A more flexible example
To proxy all .php files in your vhost to the fcgi server using their real php file locations, you can use a more flexible match:
ProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/var/www/$1
Again, assuming /var/www is the DocumentRoot of the vhost in question.
Reload apache with apachectl graceful and you can now call up the phpinfo page using http://example.com/yourscript.php
Performance and Pitfalls
mod_proxy_fcgi now supports unix domain sockets since 2.4.9 ( Unix domain socket support for mod_proxy_fcgi )
It is easy to overwhelm your system's available sockets, pass over ulimits, etc. Some tips to avoid this:
Using too many sockets will cause apache to give a (99)Cannot assign requested address: error. This means your operating system is not allowing new sockets to be created.
On linux you can use /proc/sys/net/ipv4/tcp_tw_reuse to not build up as many sockets, but there are warnings associated with using this behind a NAT.
Be sure to modify ulimit and allow for enough open files and processes for both the apache user and the php-fpm user. ulimit -n and ulimit -u (nofile and nproc)
If php-fpm does not have a large enough nproc it will exit (code 255, no additional information as of php 5.3) in a loop without additional messages.
If php-fpm does not have a large enough nofile you will probably not be able to get logging per child, as shown above. It will give this in the general error log.
If apache and php-fpm run as the same user (not necessary or recommended) and nproc is too small, apache will not startup with the following message (11)Resource temporarily unavailable: AH02162: setuid: unable to change to uid: 600
Warning: when you ProxyPass a request to another server (in this case, the php-fpm daemon), authentication restrictions, and other configurations placed in a Directory block or .htaccess file, may be bypassed.
One might be tempted to point out that a greedy ProxyPassMatch directive might allow some malicious content uploaded by a HTTP client to be served.
This is by no means a comprehensive security document, but instead will point out a possible injection vector that could be generated from the directives in this document.
Take, for example:
Would lead php-fpm to process that file (/uploads/malicious.jpg), and without certain sanity check, possibly lead to a compromised server.
This, of course, is not recommended. Content uploaded using php should be saved safely outside the DocumentRoot, and the pathinfo should be scrutinized.
Additionally, php-fpm should check if the script being invoked is allowed.