Skip to content

Installing PHP-FPM#

This page lived for years under the title "Installing PHP 7 and PHP-FPM on CentOS 7." CentOS 7 reached end of life in June 2024, and PHP 7 before that, in 2019. The old link still points here, but its content should not keep teaching something that is no longer accurate - so we rewrote the same question for current distributions and versions. The logic is the same; the commands are not.

What PHP-FPM is, and why not mod_php#

PHP-FPM (FastCGI Process Manager) runs PHP in its own worker pools, separate from the web server's process. nginx cannot execute PHP at all - unlike Apache it has no PHP module - so any nginx setup needs PHP-FPM. Even on Apache, PHP-FPM is preferred over mod_php today: it gives process isolation, independent restarts and more predictable memory use.

Which PHP version should I install#

PHP ships a new minor version every November; each one gets roughly 2 years of active support plus 1 more year of security-only fixes. "The current version" will keep changing after this page is written, so rather than a fixed number, check php.net/supported-versions - it shows whatever is actively supported right now.

Rule of thumb

For a new project, pick the newest version on that list. If you are moving an existing app that is pinned to an older version, see "Multiple PHP versions on one server" below - you do not have to spread the oldest one across the whole system.

The examples below use 8.4; swap in whichever version you actually want, the logic of each step stays the same.

1. Install the packages#

The distribution's own repositories usually lag a version or two behind. For current versions, Ondřej Surý's PPA - the de-facto standard in the PHP world since the CentOS 7 days - is still the reference:

sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y nginx php8.4-fpm php8.4-cli php8.4-mysql \
  php8.4-curl php8.4-gd php8.4-mbstring php8.4-xml php8.4-zip

If the distro's own repo is good enough for you, drop the version number from the package names (php-fpm, php-cli, …) and you get that Ubuntu/Debian release's default.

sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf module reset php
sudo dnf module enable -y php:remi-8.4
sudo dnf install -y nginx php-fpm php-cli php-mysqlnd php-curl php-gd php-mbstring php-xml php-zip

Rocky/AlmaLinux is not CentOS 7's direct successor, but it plays the same role in the RHEL family today - and the Remi repo has been doing the same job since the CentOS 7 era. Replace remi-8.4 with whatever version is current (dnf module list php shows what is available).

These commands are for EL9

The 9 in epel-release-latest-9 and remi-release-9 is specific to the major EL version (Rocky/AlmaLinux 9) - EL8 needs epel-release-latest-8 + remi-release-8, EL10 needs epel-release-latest-10 + remi-release-10. The wrong version number fails the install with dependency conflicts. Confirm your own major version with cat /etc/os-release.

With Morpheus

"install the newest php-fpm version on my server, nginx is already installed"

Morpheus recognizes the distribution, checks which versions the repo actually has, and picks the right repo/package name; on RHEL it also runs the module-enable step for you.

2. Configure the pool#

PHP-FPM defines a separate "pool" for each site or app. Where the pool file lives, and the socket path inside it, differ by distribution - mix them up and you get a 502, so here they are separately.

/etc/php/8.4/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
sudo systemctl restart php8.4-fpm
/etc/php-fpm.d/www.conf
[www]
user = nginx
group = nginx
listen = /run/php-fpm/www.sock
listen.owner = nginx
listen.group = nginx
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
sudo systemctl restart php-fpm

If you'd rather share only the socket permission than the user/group

Setting the user/group above to match nginx's is the simplest path - it causes no real problem on a single-site server. But if you want FPM's workers to keep their own identity (separate from nginx), skip changing user/group and instead set only listen.owner/listen.group/listen.mode to match the user nginx runs as - socket permission and worker identity are two different things, and you can fix the former without touching the latter.

3. Wire it into nginx#

/etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com;
    root /var/www/example.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}

Debian/Ubuntu's nginx package only loads what's in sites-enabled/ - writing to sites-available/ is not enough on its own, you need to enable it with a symlink:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t && sudo systemctl reload nginx

The RHEL family's nginx package does not use sites-available/sites-enabled - everything goes straight into conf.d/ as a plain .conf file, no separate enable step needed. snippets/fastcgi-php.conf is also Debian-package-specific; on RHEL you add the fastcgi parameters by hand:

/etc/nginx/conf.d/example.com.conf
server {
    listen 80;
    server_name example.com;
    root /var/www/example.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}
sudo nginx -t && sudo systemctl reload nginx

fastcgi_pass must point at the exact same socket you wrote for listen in step 2. The smallest typo between the two is a 502 Bad Gateway - the single most common PHP-FPM error there is.

4. Test it (and delete it right away)#

echo "<?php phpinfo();" | sudo tee /var/www/example.com/info.php

Open https://example.com/info.php to see the PHP version and loaded modules.

Do not leave info.php in place

phpinfo() leaks server paths, loaded modules and sometimes environment variables. Delete it the moment you are done:

sudo rm /var/www/example.com/info.php

Multiple PHP versions on one server#

If an old app is pinned to an older version while a new one wants the current release (say, 8.1 alongside 8.4), you can install both side by side - each version gets its own FPM pool and its own socket (php8.1-fpm.sock, php8.4-fpm.sock), and nginx's fastcgi_pass picks the right one per site. The system-wide php command is bound to a single version; update-alternatives --config php (Debian) switches the CLI side. Keep the old version scoped to that one app - once its security support ends, do not pick it again for a new site.

Troubleshooting#

502 Bad Gateway - almost always a socket/port mismatch. nginx's fastcgi_pass and the FPM pool's listen do not match, or the FPM service is not running at all (systemctl status php8.4-fpm on Debian/Ubuntu, systemctl status php-fpm on the RHEL family).

"Permission denied" in the FPM log - user/group does not match the actual server user, or file permissions are too restrictive. Check the log for the exact path it's failing on and fix ownership of only that path (chown www-data:www-data /var/www/example.com/that-file) - blindly running chown -R across the whole site root leaves your application code under the same ownership as uploaded content, so a compromised web process can also write to your code files.

Changes do not take effect - you may need to restart FPM rather than reload; pool file changes are only read on restart.

With Morpheus

"check why php-fpm is returning 502"

Morpheus checks the nginx error log, the FPM service status and whether the socket file exists, and tells you which one is missing.

Checklist#

  • [ ] systemctl status php8.4-fpm (Debian/Ubuntu) or systemctl status php-fpm (RHEL family) shows active (running)
  • [ ] nginx -tsyntax is ok
  • [ ] fastcgi_pass and the FPM listen point at the same socket
  • [ ] info.php was tested and deleted
  • [ ] user/group match the user nginx actually runs as