Moving Drupal 7 from Apache mod_php and prefork to PHP-FPM and event on Ubuntu 16.04

I recently changed this site to use PHP-FPM/mod_proxy_fcgi (from mod_php) and event MPM (from prefork). I found many online resources to help, but there wasn't a single page that listed all the steps. I had to cobble them together from several different sources. I'm writing this post to help anyone else in a similar situation.

The original reason I wanted to make these changes was to enable HTTP/2. HTTP/2 is incompatible with Apache's prefork MPM, and mod_php requires prefork. Along the way I discovered that both prefork and mod_php are deprecated and shouldn't be used any longer. Furthermore, there are performance benefits to using the event MPM and PHP-FPM/mod_proxy_fcgi.

The rest of this post is detailed instructions about how to make the change. The initial state was:

  • Ubuntu 16.04
  • Apache 2.4
    • mod_php
    • prefork MPM
  • PHP 5.6
  • Drupal 7.69

First, backup your site. In addition to my usual backups, I created an AWS EC2 AMI that functioned as a backup of the entire state of the webserver.

I started by updating everything (optional):

sudo apt update
sudo apt upgrade

Switching to PHP-FPM

The first major step is to switch from mod_php to PHP-FPM. The reason to do that first is that you can test your site under the prefork/PHP-FPM combination to make sure the switch to PHP-FPM worked. First, install PHP-FPM:

sudo apt install php5.6-fpm

At this point I setup the PHP configuration for PHP-FPM to match what I had before with mod_php. On Ubuntu 16.04, the default configuration is in /etc/php/5.6. I compared the apache2 and fpm directories therein to see what was different (diff -ur helped here). In my case, I had enabled one extra module for mod_php, so I enabled it for FPM too (the modules you are using are likely to be different):

sudo phpenmod -s fpm jsmin

For the rest of the settings, I copied them from the mod_php ones:

/etc/php/5.6/fpm$ sudo cp ../apache2/php.ini .

Apache can also be configured to change PHP settings, but that only works with mod_php. These may be set in .htaccess files or Apache configuration files. In my case I had some that Drupal includes in its .htaccess file. I added these to /etc/php/5.6/fpm/php.ini:

magic_quotes_gpc = Off
magic_quotes_sybase = Off
register_globals = Off
session.auto_start = Off
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off

At this point I turned on mod_proxy_fcgi:

sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php5.6-fpm
sudo systemctl restart apache2

Remember that last command (and its companion sudo systemctl restart php5.6-fpm for FPM.). You will likely find yourself needing to restart Apache several times during this process (in particular, whenever you change a config file).

Optional Apache Configuration

I made some improvements to how Apache connects to PHP-FPM. These are optional. Edit /etc/apache2/conf-enabled/php5.6-fpm.conf. It should start with something like

<IfModule !mod_php5.c>
<IfModule proxy_fcgi_module>
    # Enable http authorization headers
    <IfModule setenvif_module>
    SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1
    </IfModule>

    <FilesMatch ".+\.ph(p[3457]?|t|tml)$">
        SetHandler "proxy:unix:/run/php/php5.6-fpm.sock|fcgi://localhost"
    </FilesMatch>

After the </FilesMatch> section add:

    <Proxy "fcgi://localhost" max=10>
      ProxySet connectiontimeout=5 timeout=240
    </Proxy>

to control how many simultaneous connections Apache will make to PHP-FPM, and when Apache should timeout. Change

SetHandler "proxy:unix:/run/php/php5.6-fpm.sock|fcgi://localhost"

to

<If "-f %{REQUEST_FILENAME}">
        SetHandler "proxy:unix:/run/php/php5.6-fpm.sock|fcgi://localhost"
</If>

That prevents Apache from trying to run PHP on script files that don't exist, which leads to strange error messages. At this point (and later) you can run sudo apachectl configtest to check if your Apache configuration has syntax errors (it will print "Syntax OK" if the configuration files are valid).

Testing PHP-FPM

Now actually turn off mod_php and use PHP-FPM (disabling mod_php switches to using FPM because of the <IfModule !mod_php5.c> in php5.6-fpm.conf):

sudo a2dismod php5.6
sudo systemctl restart apache2
sudo systemctl restart php5.6-fpm

At this point you should be able to see your website working with PHP-FPM. In the Drupal Status report, if you click "more information" on the PHP line, it will tell you whether it is using FPM (look for "Server API").

If you're using the cdn module you might encounter a bug at this point: some resources (e.g. images) served by the CDN don't show up. The bug causes the HTTP Cache-Control header to be malformed. I think this bug manifested because mod_php internally removes the malformed header, but mod_proxy_fcgi is more strict and throws an error when PHP sends the malformed header. In any case, if you encounter this problem, first try updating your cdn module. However, as of version 7.x-2.10 the bug fix was not included so that may not work. I fixed it by manually applying the code patch: find file sites/all/modules/cdn/cdn.basic.farfuture.inc and change the line

header('Cache-Control', 'max-age=290304000, no-transform, public, immutable');

to

header('Cache-Control: max-age=290304000, no-transform, public, immutable');

If you added the <If "-f %{REQUEST_FILENAME}"> part above, you may notice that the PHP execution prevention for the files directory has stopped working. (You can test this using the security review module.) This happens because Apache applies the <If> directive in php5.6-fpm.conf after the <Files> directive in the .htaccess file in the files directory. To fix it, in sites/default/files/.htaccess change

<Files *>
  # Override the handler again if we're run later in the evaluation list.
  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>

to

<Files *>
  # Override the handler again if we're run later in the evaluation list.
  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
  # PHP-FPM SetHandler is inside an If
  # which is applied after Files. Need to be inside another If to override.
  # only block php, not other files e.g. advagg css
  <If "%{REQUEST_URI} =~ /.+\.ph(p[3457]?|t|tml)$/">       
     SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
  </If>
</Files>

That should make the PHP execution prevention work again. However, if you're using Security Review, it may still complain that the .htaccess file has been modified. After testing that the PHP execution prevention was working, I disabled that test to silence the warning because I knew I had modified the file. (I recommend running the test again whenever the PHP or Apache configuration changes.)

Switching Apache to event MPM

Once you have everything working with PHP-FPM and mod_proxy_fcgi, you can switch Apache's MPM:

sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo systemctl restart apache2

At this point, the site should be working with PHP-FPM and event MPM. You might want to tune the number of workers for FPM and Apache. For FPM, the config file is /etc/php/5.6/pool.d/www.conf. For my tiny site I use

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.max_spare_servers = 3

Some of the references have information about how many PHP processes to use. It's a tradeoff between how many simultaneous connections you want to support and how much RAM your server has. While you're in that file, you can also add

php_admin_value[error_log] = /var/log/fpm-php.www.log
php_admin_flag[log_errors] = on

so you have a log of errors from PHP. You need to sudo systemctl restart php5.6-fpm so these changes have effect. The corresponding configuration for Apache is in /etc/apache2/mods-enabled/mpm_event.conf. I didn't need to change any of these from the defaults (you might if your site gets more traffic). Because Apache is no longer running PHP internally, the memory needed for each Apache process is much lower. The event MPM allows each process to handle many more requests simultaneously, so you will probably get a boost in server capacity just by switching to event MPM.

References

PHP-FPM page from the Apache community wiki overall this had the most useful information about setting up PHP-FPM and mod_proxy_fcgi

Running Drupal on Debian 9 with Apache 2.4, HTTP/2, event MPM and PHP-FPM (via socks and proxy) this was one of the most complete guides, with good information about the various config files.

General information on PHP in Apache from the Apache community wiki. Useful mainly for understanding the differences among mod_php, mod_proxy_fcgi, prefork, and event.

Moving from mod_php to PHP-FPM (on Fedora)

A serverfault question that helped me understand how to configure the Proxy

Apache documentation for mod_proxy_fcgi useful for understanding what the various proxy settings mean

P.S. Random debugging tip

If you need to see what PHP is returning directly, without passing through Apache, you can run

export SCRIPT_FILENAME=/full/path/to/index.php
export REQUEST_URI='/URL/you/want/to/test/like/sites/default/files/image/an_image.jpg'
export QUERY_STRING=
export REQUEST_METHOD=GET
export REMOTE_ADDR=127.0.0.1
export SERVER_SOFTWARE=apache
cgi-fcgi -bind -connect /run/php/php5.6-fpm.sock

You may need to sudo apt install libfcgi0ldbl to get the command.

Blog Categories: