2026年11月12日

2026年11月12日

Apache with PHP-FPM: mod_proxy_fcgi Setup and Fixing 503 Errors

This article is a localized edition of the Japanese original. Read the Japanese original

There are two ways to run PHP with Apache: mod_php, which embeds PHP into Apache, and handing PHP off to PHP-FPM, a separate process. The latter is the mainstream approach today: Apache 2.4's mod_proxy_fcgi passes PHP requests to PHP-FPM. This article explains how to switch from mod_php to PHP-FPM, how to give each site its own pool, and the pitfalls WordPress sites often hit after the switch (503 errors and php_value in .htaccess) and how to check for them.

Why Use PHP-FPM Instead of mod_php

mod_php loads PHP into Apache's processes, so even processes that only serve images or CSS use extra memory for PHP. mod_php is also hard to combine with threaded MPMs (event or worker) and tends to lock you into the prefork MPM. Prefork uses one process per connection, so memory consumption grows as concurrent access increases.

When you separate PHP into PHP-FPM, Apache concentrates on static files and connection handling with the event MPM, while PHP-FPM's workers take care of running PHP. The advantages include:

  • Apache can run with the event MPM, which makes it easier to handle concurrent connections with less memory
  • You can tune the number of PHP workers separately from Apache
  • You can create a separate pool for each site, with its own execution user and php.ini values
  • You can swap PHP versions independently of Apache

On the other hand, the configuration is split between two places, Apache and PHP-FPM, and it is easy to get stuck on the permissions of the socket that connects them. The steps below proceed with that in mind.

Preparation Before You Start

If you are working on a production server, back up the configuration files beforehand and, if possible, also take a server snapshot. PHP will not run for a short time during the switch, so it is safest to go through it once on a staging environment and then do it on production at a time of low traffic.

# Debian/Ubuntu family
sudo tar czf ~/apache-php-backup-$(date +%F).tar.gz /etc/apache2 /etc/php
# RHEL/AlmaLinux family
sudo tar czf ~/apache-php-backup-$(date +%F).tar.gz /etc/httpd /etc/php-fpm.d /etc/php.ini

Also check your current setup. apache2ctl -M (httpd -M on the RHEL family) lists the loaded modules: if php_module is there, you are using mod_php, and entries such as mpm_prefork_module tell you the MPM.

sudo apache2ctl -M | grep -E 'php|mpm|proxy'

Steps on the Debian/Ubuntu Family

The examples below use PHP 8.3. The standard package on Ubuntu 24.04 is PHP 8.3, but the PHP version differs by distribution release (as of October 2026). Replace "8.3" in package names, service names and socket paths with the version you actually have installed.

1. Install PHP-FPM

sudo apt update
sudo apt install php8.3-fpm
systemctl status php8.3-fpm

The extensions WordPress uses (php8.3-mysql, php8.3-xml, php8.3-mbstring, php8.3-curl, php8.3-gd or php8.3-imagick, php8.3-zip, php8.3-intl and so on) are shared between mod_php and PHP-FPM. If they are already installed, you do not need to add them. However, php.ini is a separate file for each SAPI: mod_php reads /etc/php/8.3/apache2/php.ini, and PHP-FPM reads /etc/php/8.3/fpm/php.ini. If you had changed upload_max_filesize or memory_limit, apply the same values to the FPM php.ini as well. If you forget this, the upload limit will appear to have reverted after the switch.

2. Switch the Apache Modules

sudo a2dismod php8.3
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo apache2ctl configtest
sudo systemctl restart apache2

The configuration file enabled by a2enconf php8.3-fpm contains a SetHandler that passes requests for .php files to the PHP-FPM socket. Its content looks roughly like this:

<FilesMatch ".+\.ph(?:ar|p|tml)$">
    SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>

In proxy:unix:...|fcgi://localhost, the first half is the location of the socket and the second half is an identifier for the backend. To listen over TCP instead of a socket, write something like proxy:fcgi://127.0.0.1:9000. On the same server, a socket involves less overhead and cannot be reached from outside.

If mod_php is still loaded when you switch MPMs, apache2ctl configtest or the restart reports an error saying it cannot be combined with a threaded MPM. In that case, check that you removed the PHP module with a2dismod.

Steps on the RHEL/AlmaLinux Family

The httpd package on the RHEL family is set up to work with PHP-FPM by default. Installing the php-fpm package provides a configuration equivalent to SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost" in /etc/httpd/conf.d/php.conf, so in many cases the following is enough to get it running.

sudo dnf install php-fpm php-mysqlnd
sudo systemctl enable --now php-fpm
sudo httpd -t
sudo systemctl restart httpd

php.ini is /etc/php.ini, and the pool configuration is /etc/php-fpm.d/www.conf. Check that the default pool's execution user is apache and not a value meant for nginx. On systems with SELinux enabled, if you moved the socket or put the document root in a custom location, access may be denied because of SELinux labels rather than permissions. You can check the denial records with commands such as ausearch -m avc -ts recent.

Check That It Works

Once the switch is done, check that PHP is running through PHP-FPM. In the WordPress admin, go to "Tools" → "Site Health" → "Info"; if the "Server" section shows fpm-fcgi as the PHP SAPI, PHP is running through PHP-FPM (the wording may change between WordPress versions).

To check from the command line, you can also temporarily place a PHP file. Delete it right after checking. phpinfo output contains internal server information, so you must not leave it publicly accessible.

<?php
// Delete immediately after checking
echo PHP_SAPI; // if it shows fpm-fcgi, PHP is running through PHP-FPM

Give Each Site Its Own Pool

When you run several WordPress sites on one server, creating a PHP-FPM pool for each site and running each under a different user reduces the risk that, if one site is compromised, the other site's files are written to.

; /etc/php/8.3/fpm/pool.d/site1.conf
[site1]
user = site1
group = site1
listen = /run/php/php8.3-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M

user is the user that runs PHP, and listen.owner and listen.group are the owner of the socket file. Apache (www-data on the Debian family) must be able to read and write the socket, so specify these separately from the PHP execution user. pm.max_children is the maximum number of PHP processes running at the same time; decide it from the memory used per process and the server's memory. Start with a small value and adjust it while watching the error logs described below.

In the VirtualHost, configure this site alone to pass requests to its dedicated socket.

<VirtualHost *:443>
    ServerName site1.example.com
    DocumentRoot /var/www/site1

    <FilesMatch ".+\.php$">
        SetHandler "proxy:unix:/run/php/php8.3-fpm-site1.sock|fcgi://localhost"
    </FilesMatch>
</VirtualHost>

Check the pool configuration syntax with sudo php-fpm8.3 -t, then apply it with sudo systemctl reload php8.3-fpm. As a rule, files in the document root should be readable by the PHP execution user (site1 here), with only wp-content/uploads, used for uploads, writable.

Common Pitfalls After the Switch

You Get 503 Service Unavailable

If only PHP pages return 503, Apache cannot connect to the PHP-FPM socket. Apache's error log (/var/log/apache2/error.log on the Debian family, /var/log/httpd/error_log on the RHEL family) records a message saying the connection to the socket failed. Check the following, most likely first.

  1. PHP-FPM is not running: check with systemctl status php8.3-fpm, and use journalctl -u php8.3-fpm to see whether it failed to start because of a configuration error.
  2. The socket path does not match: check the actual socket name with ls -l /run/php/ and compare it with the path in SetHandler. A common case is the old path being left in place after upgrading PHP.
  3. The socket permissions are insufficient: if the error log shows Permission denied, review listen.owner, listen.group and listen.mode.

php_value in .htaccess Causes a 500 Error

Sites that added lines such as php_value upload_max_filesize 64M to .htaccess in the mod_php days get a 500 error after switching to PHP-FPM. php_value is a directive provided by mod_php, so once mod_php is removed, Apache treats it as a directive it cannot interpret. The error log shows a message like Invalid command 'php_value'.

Delete those lines and move the values to the pool configuration (php_admin_value or php_value) or to a .user.ini placed in the document root. PHP-FPM reads .user.ini, but by default it only rereads it every few minutes, so if a change does not take effect right away, wait calmly or reload PHP-FPM.

Code Changes Do Not Take Effect

With PHP-FPM, OPcache is held per FPM process. Depending on the settings, the old cache may continue to be used even after you update a file; in that case, systemctl reload php8.3-fpm fixes it. Note that restarting Apache does not clear the cache on the PHP-FPM side.

Heavy Pages Time Out

With PHP-FPM, in addition to PHP's max_execution_time, Apache's proxy wait times (Timeout and ProxyTimeout) and PHP-FPM's request_terminate_timeout come into play. If long-running tasks such as imports are cut off partway, use the error logs to identify which stage's timeout is responsible before adjusting the values.

Summary

  • Connect Apache and PHP-FPM with mod_proxy_fcgi's SetHandler "proxy:unix:socket|fcgi://localhost"
  • On the Debian/Ubuntu family, remove mod_php and prefork, and enable the event MPM, proxy_fcgi and the php-fpm conf. On the RHEL family, installing and starting php-fpm is enough for them to work together by default
  • php.ini is a separate file for each SAPI. Move your values to the FPM php.ini or the pool configuration
  • For multiple sites, separate the pools and use a different execution user and socket for each
  • For a 503, first suspect whether the socket is running, its path and its permissions; for a 500, first suspect php_value in .htaccess
  • Back up before you start, verify on staging, then apply to production

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲