PHP-FPM Config for Shared Hosting
Blog post created on 2020-05-27
Over at @dodify we are managing a couple of web servers handling 40+ websites each. One of these web servers is configured as a shared hosting environment and runs all the standalone PHP app websites that we have inherited over the years, not limited to WordPress, PrestaShop or little custom PHP apps.
The server is not a traditional shared hosting setup, as only we have access to it, and is not running any web panel or related software. It is clean and tidy and configured with minimal software from the command line. Each site runs under their own user and we have nifty scripts to add/remove sites as necessary as Apache virtual hosts.
I recently decided to migrate the server to DigitalOcean, and took the opportunity to update the server setup and try out PHP-FPM
(was previously using mod_php
). Given these are pretty small sites, we are trying to squeeze as much as we can out of the droplet that is currently configured with 8GB or RAM. After I got the default PHP-FPM config going I was ready to move on as things were looking good.
Out of Memory
Little did I know that after a couple of days I would receive an alert from uptime robot notifying me that the websites were down. Turns out I had run out of memory causing MariaDB to fail. Restarting PHP-FPM fixed the problem but of course it was only a matter of time before it would happen again. I tried being lazy but it was time to read the manual.
It turns out that the default PHP-FPM configuration is optimised for single application servers that prefer pure performance over memory management, not exactly the shared hosting use case.
Default Configuration
Each site on the server has its own user and is configured as a vhost
on Apache. Each site also gets its own php-fpm
configuration file stored under /etc/php-fpm.d
. Within the config file the process manager configuration is set. The process manager is responsible for handling requests and creating/destroying child processes, and is therefore a main component of the setup. By default the relevant variables were set like so:
pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 5 pm.max_spare_servers = 35 ;pm.process_idle_timeout = 10s;
When the pm
variable is set to dynamic
the number of child processes are set dynamically based on the other directives shown above. With this process management setting, there will be always at least 1 child process active. Note also that the process_idle_timeout
is commented out as it is not used in this case.
This configuration seemed good at first and performance was reasonable, however, over time, as the sites received traffic, the process manager continued to add more processes to handle the requests, eventually using up all memory. It is also still not clear to me how the process manager will (if it ever does) shut down idle processes. Note of course that this configuration is repeated for each site on the server.
Technically PHP-FPM does allow you to set a memory usage limit, but given this is still configured on a per site basis I would have had to calculate total memory / number of sites
and updated all relevant settings every time a new site was added - not ideal.
Optimising
Turns out the solution was not only extremely simple, but also very good for my use case. I changed the following two lines in the config file:
pm = ondemand pm.process_idle_timeout = 10s;
With the pm
setting configured to ondemand
no children processes are created at startup. Rather, child processes will be forked when new requests will connect. This does mean a slightly delayed start time, but given the simplicity of these PHP sites and the low traffic, this is essentially negligible for me. I expect this would likely be the same for other standard shared hosting setups as well. Additionally, when using ondemand
the following parameters are used: pm.max_children
, the maximum number of children that can be alive at the same time. I left this at 50 so that I could easily handle spikes in traffic, and pm.process_idle_timeout
, the number of seconds after which an idle process will be killed. By default this latter setting is commented out and I left this at 10 seconds as it seemed reasonable.
Memory usage has not given me a problem since. It normally stays well below 2GB and ramps when needed, and if required when sites receive traffic. With this config I'm also able to serve sudden bursts of traffic for a subset of sites at the same time, without the risk of hitting the memory cap. And as soon as the traffic is gone, memory usage slowly decreases back to normal levels.
All in all I'm satisfied with PHP-FPM and I'm expecting to be able to run 4x the number of sites with the ondemand
process manager compared to dynamic
without increasing memory.