Skip to content

PHP-FPM

Installing PHP-FPM

Add the PPA:

sudo apt update && sudo apt install software-properties-common
sudo add-apt-repository ppa:ondrej/php
Install PHP 7.4 and PHP-FPM 7.4:

sudo apt update && sudo apt install php7.4 php7.4-fpm php7.4-cli

Amend the php.ini file:

sudo nano /etc/php/7.4/cli/php.ini

Change the following global settings for PHP:

date.timezone = "Europe/London"
expose_php = Off
display_errors = off
log_errors = on
file_uploads = On
upload_max_filesize = 2M
post_max_size = 2M
max_execution_time = 300
allow_url_fopen = off

Enable the fpm service:

sudo systemctl enable php7.4-fpm --now

Disable Mod_php and mpm_prefork, and enable mpm_event (mpm_event should be enabled by default but we'll do this just to make sure):

sudo a2dismod mpm_prefork
sudo a2enmod mpm_event

Enable modules required for PHP-FPM:

sudo a2enmod alias proxy_fcgi setenvif

Enable PHP-FPM configuration:

sudo a2enconf php7.4-fpm

Add the required entries to the Virtualhost config:

sudo nano /etc/apache2/sites-available/example.com.conf
<VirtualHost 127.0.0.1:8080>

    ...

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/var/run/php/php7.4-fpm.sock|fcgi://localhost"
    </FilesMatch>

    ...

</VirtualHost>

Restart Apache2:

sudo systemctl restart apache2

PHP Pools

The default PHP-FPM pool configuration is found at /etc/php/7.4/fpm/pool.d/www.conf

Unless further amendments are made, all PHP sites created will use this single pool. The pool configuration file contains the following 4 settings:

; pool name
[www]
...
; Unix user/group of processes
; will be used.
user = www-data
group = www-data
...
; Socket to which the Apache will connect
listen = /run/php/php7.4-fpm.sock
These settings need to be unique on each pool configured. For security reasons aswell as better resource management, it's a good idea to have each PHP site run using its own pool and user ID.

For example, let’s create another pool called example to be used with our example.com website. Make sure that beforehand you create the user and group that will be used by the new pool. Create them with the commands:

sudo groupadd example
sudo useradd -g example example

Create a temp directory in your app folder:

mkdir -p /var/www/example.com/html/tmp
chown -R example:example /var/www/example.com/html/tmp

Now copy the pool file www.conf to example.conf:

sudo cp /etc/php/7.4/fpm/pool.d/www.conf /etc/php/7.4/fpm/pool.d/example.conf

Now edit the new file and amend as follows:

; pool name
[example]
...
; Unix user/group of processes
; will be used.
user = example
group = example

listen.owner = www-data
listen.group = www-data
listen.mode = 0660
...
; Socket to which the Apache will connect
listen = /run/php/php7.4-fpm-example.sock

; Restrict PHP scripts to app folder
; Extra folder paths can be added, seperated with a colon
; Or the restriction can be removed by specifying none
php_admin_value[open_basedir] = /var/www/example.com/html
php_admin_value[sys_temp_dir] = /var/www/example.com/html/tmp
php_admin_value[upload_tmp_dir] = /var/www/example.com/html/tmp
;
; Some php settings can be set per pool, some examples are below.
; Uncomment and amend as appropriate
;
;php_admin_value[memory_limit] = 512M
;php_admin_flag[output_buffering] = Off
;php_admin_value[max_execution_time] = 1800
;php_admin_value[max_input_time] = 3600
;php_admin_value[post_max_size] = 2M
;php_admin_value[upload_max_filesize] = 2M
;php_admin_value[max_file_uploads] = 100
;php_admin_flag[session.cookie_secure] = True
;
; Disable unneeded and potentially dangerous functions, eg:
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,dl,setenv

And finally restart the PHP-FPM service:

sudo service php7.4-fpm restart

The Apache virtualhost should also be edited to include the new socket used by the new pool:

sudo nano /etc/apache2/sites-available/example.com.conf
<FilesMatch \.php$>
    SetHandler "proxy:unix:/var/run/php/php7.4-fpm-example.sock|fcgi://localhost/"
</FilesMatch>

Now change recursively the ownership of the Virtualhost directory:

sudo chown -R example:www-data /var/www/example.com

And set the permissions:

sudo chmod -R 740 /var/www/example.com

Check your websites documentation. Some folders may need to be writable by the web server. If so then change them like:

sudo chmod -R 770 /var/www/example.com/html/writable-folder

PHP files can be further restricted like so:

find /var/www/example.com/html -type f -name '*.php' -exec chmod 600 {} \;

Tuning Apache2 and PHP-FPM

This involves a bit of calculating.

First we install the py_mem utility:

sudo -s
python3 -m pip install py_mem

The first thing we need to know is how many cores we have available for use:

lscpu | egrep 'Model name|Socket|Thread|NUMA|CPU\(s\)'

Sample output:

CPU(s):                          4
On-line CPU(s) list:             0-3
Thread(s) per core:              2
Socket(s):                       1
NUMA node(s):                    1
Model name:                      Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz
NUMA node0 CPU(s):               0-3

Here we can see that there are 4 physical cores and 2 threads per core, so we class this as 8 logical cores.

Now we find the total available RAM:

vmstat -s -S M | grep "total memory"

Sample output:

31854 M total memory

We need to make a decision on how much of this RAM to allocate in total to PHP-FPM processes. If you have other services running such as a mail server, database, etc, then you'll want to keep a fair amount in reserve. For this example, we'll allocate 16 GB of RAM to PHP-FPM.

Now let's say we have 2 large PHP apps being served, each with their own dedicated PHP Pool. We'll call these app1 and app2. For this example, let's allocate 10 GB of the 16 GB to app1 and the remaining 6 GB to app2.

We'll now find out how many processes Apache2 is running, and how much memory it's using per process:

ps_mem | grep 'apache2'

This will output something like:

10.5 MiB +  40.2 MiB =  50.8 MiB    apache2 (8)

So here we see that Apache2 is using a total of 50.8 MB of memory over 8 processes

Now we'll check the usage of the app1 and app2 pools. We'll assume that the users of the pools are called www-app1 and www-app2:

ps_mem -p $(pgrep -d, -u www-app1) | grep 'php-fpm7.4'
ps_mem -p $(pgrep -d, -u www-app2) | grep 'php-fpm7.4'

Sample output:

371.5 MiB + 456.0 MiB = 827.5 MiB   php-fpm7.4 (32)
256.6 MiB + 383.3 MiB = 639.9 MiB   php-fpm7.4 (32)

So app1 is using 827.5 MB over 32 processes, and app2 is using 639.9 MB over 32 processes.

We'll use all these values to set the values in the mpm_event.conf and php-fpm pool config.

First we'll do the Apache mod_mpm_event settings. Here are the calculations:

ServerLimit         = MaxRequestWorkers / ThreadsPerChild 
StartServers        = Number of cores (in our case, 8) 
ThreadsPerChild     = 25  
MaxRequestWorkers   = Total allocated memory / average memory per process 
                       (rounded up to the nearest multiple of ThreadsPerChild)
MinSpareThreads     = MaxRequestWorkers / 2
MaxSpareThreads     = MaxRequestWorkers

So in our examples, this would work out as:

ServerLimit         104
StartServers        8
ThreadsPerChild     25
MaxRequestWorkers   2600
MinSpareThreads     1300
MaxSpareThreads     2600

Now we'll do the pool settings for app1 and app2.

The setting we tune are:

pm.max_children         = Total RAM allocated to pool / average memory per pool process
pm.start_servers        = Number of cores x 4
pm.min_spare_servers    = Number of cores x 2
pm.max_spare_servers    = Number of cores x 4

So in our examples, this work work out as:

[app1]
pm.max_children = 396
pm.start_servers = 32
pm.min_spare_servers = 16
pm.max_spare_servers = 32
[app2]
pm.max_children = 308
pm.start_servers = 32
pm.min_spare_servers = 16
pm.max_spare_servers = 32

Restart php-fpm and apache2 when finished tuning:

sudo systemctl restart apache2 && systemctl restart php7.4-fpm