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.
php-fpm
Prerequisites:
- 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!
Pay special attention to the slowlog settings (request_slowlog_timeout and slowlog directives), that is, through this log and a reasonable amount of timeout, you can easily see which php calls from your application take longer than expected and start debugging them right away.
Let's just use the defaults as shipped and start the php-fpm daemon; if your distro uses the provided init script, run/etc/init.d/php-fpm start
Or if not, start it manually withphp-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:error_log /var/log/php-fpm.log
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
prerequisites:
- 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:
ProxyPassMatch
^
(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.
fcgi://127.0.0.1:9000
/path/to/your/documentroot/
IMPORTANT! Read the above again
$1
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://localhost/path/to/your/documentroot/
unix:/path/to/socket.sock
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>
</Proxy>
<FilesMatch "\.php$">
{{ <If "-f %{REQUEST_FILENAME}">}}
{{ # Pick one of the following approaches}}
{{ # Use the standard TCP socket}}
{{ #SetHandler "proxy:fcgi://localhost/:9000"}}
{{ # If your version of httpd is 2.4.9 or newer (or has the back-ported feature), you can use the unix domain socket}}
{{ #SetHandler "proxy:unix:/path/to/app.sock|fcgi://localhost/"}}
{{ </If>}}
</FilesMatch>
Combining FilesMatch and If can achieved as such:
<If "-f %{REQUEST_FILENAME} && %{REQUEST_URI} =~ /.+\.ph(ar|p|tml)$/" >
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.
Caveats
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:
/uploads/malicious.jpg/lalalaalala.php
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.
If such restrictions cannot be implemented easily, then checks could be performed prior to proxying with a RewriteCond or FallbackResource to ensure that the URI is not altered by the HTTP client.
Experiments with timeouts
For long-running scripts, setting a large timeout might help. This approach will set the time out value to 300 seconds, and allow you to be selective about what requests are proxied:
<Proxy "unix:/var/run/php-fpm/example.com.sock|fcgi://localhost">
ProxySet timeout=300
</Proxy>
<FilesMatch \.php$>
SetHandler "proxy:fcgi://localhost"
</FilesMatch>