Code Locket Menu

PHP-FPM Config for Shared Hosting

Blog post created on 2020-05-27

Sys AdminPHPWeb

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.