Skip to content

PHP and PHP-FPM

PHP powers a large share of the dynamic web (WordPress, Laravel, Drupal, and more). The modern way to run it is PHP-FPM (FastCGI Process Manager) — a standalone pool of PHP workers that your web server talks to over FastCGI. This page covers installing PHP/PHP-FPM, wiring it into nginx and Apache, tuning php.ini, and testing.

Tested on

AlmaLinux 9 / RHEL 9 with PHP 8.x from the AppStream module. Debian/Ubuntu notes are inline.

LAMP vs LEMP

These are the two classic PHP stacks:

Stack Components Web server
LAMP Linux + Apache + MySQL/MariaDB + PHP Apache
LEMP Linux + Enginx (the "E" is the nginx sound) + MySQL/MariaDB + PHP nginx

The difference is just the web server. In both modern variants, PHP runs as PHP-FPM rather than embedded in the web server process.

Install PHP and PHP-FPM

RHEL 9 packages PHP as an AppStream module. See which versions (streams) are available:

dnf module list php

You will see streams like 8.1 and 8.2. Optionally pin one, then install PHP, the FPM service, and the most common extensions:

# Optional: select a specific stream (e.g. 8.2)
sudo dnf module enable -y php:8.2

# Core PHP, FPM, and frequently-needed extensions
sudo dnf install -y php php-fpm php-mysqlnd php-gd php-mbstring php-xml php-curl

# Enable and start the FPM service
sudo systemctl enable --now php-fpm

php -v
Extension Why you usually need it
php-mysqlnd MySQL/MariaDB database driver
php-gd Image manipulation (thumbnails, captcha)
php-mbstring Multibyte/UTF-8 string handling
php-xml XML / many framework dependencies
php-curl Outbound HTTP from PHP
sudo apt update
sudo apt install -y php-fpm php-mysql php-gd php-mbstring php-xml php-curl
sudo systemctl enable --now php8.2-fpm    # service name includes the version
php -v

The FPM service is versioned, e.g. php8.2-fpm. Check with systemctl list-units 'php*-fpm*'.

Where does PHP-FPM listen?

On RHEL/AlmaLinux the default pool (/etc/php-fpm.d/www.conf) listens on a Unix socket:

grep '^listen' /etc/php-fpm.d/www.conf
# listen = /run/php-fpm/www.sock

On Debian it is typically /run/php/php8.2-fpm.sock. A Unix socket is faster and more secure than a TCP port for same-host communication. Note the path — you reference it from the web server below.

Wire PHP into nginx (LEMP)

Nginx has no PHP module; it must proxy PHP requests to PHP-FPM over FastCGI. Add a location that matches .php files to your server {} block:

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

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

    # Hand .php requests to PHP-FPM
    location ~ \.php$ {
        # Pull in the standard FastCGI parameter mappings
        include        fastcgi_params;

        # Pass to the PHP-FPM Unix socket (RHEL path; Debian differs)
        fastcgi_pass   unix:/run/php-fpm/www.sock;

        # The single most important parameter: the actual script to run
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;

        fastcgi_index  index.php;
    }

    # Optional but recommended: never serve hidden files like .htaccess, .git
    location ~ /\.(?!well-known) {
        deny all;
    }
}

SCRIPT_FILENAME is mandatory

Without SCRIPT_FILENAME, PHP-FPM does not know which file to execute and you get a blank page or "No input file specified". The line above derives it from the request — keep it.

SELinux for the socket and outbound calls

On RHEL/AlmaLinux, nginx talking to the FPM Unix socket works under the default policy. But if your PHP app connects out (e.g. to a database on another host, or an external API), allow it persistently: sudo setsebool -P httpd_can_network_connect 1. And remember custom roots still need semanage fcontext -t httpd_sys_content_t + restorecon — see SELinux.

Then:

sudo nginx -t && sudo systemctl reload nginx

Wire PHP into Apache

Apache has two options:

Works with any MPM (including the default event MPM on RHEL 9) and keeps PHP out of Apache's process. On RHEL the php package installs a drop-in (/etc/httpd/conf.d/php.conf) that already wires this up. The core directive looks like:

<FilesMatch \.php$>
    # Proxy PHP scripts to the FPM Unix socket
    SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost"
</FilesMatch>

Enable mod_proxy_fcgi (on Debian: sudo a2enmod proxy_fcgi setenvif && sudo a2enconf php8.2-fpm).

Option B — mod_php (legacy)

Embeds PHP directly in Apache. It requires the prefork MPM (it is not thread-safe), uses more memory, and is no longer the default on RHEL 9. Prefer Option A unless you have a specific reason.

After either option:

sudo apachectl configtest && sudo systemctl reload httpd

php.ini essentials

The active php.ini for FPM is /etc/php.ini on RHEL (Debian: /etc/php/8.2/fpm/php.ini). Confirm which file is loaded with php-fpm -i | grep 'Loaded Configuration File' (or check a phpinfo() page).

Settings you will almost always review:

; Maximum memory a single script may use
memory_limit = 256M

; Largest file a user can upload
upload_max_filesize = 64M

; Total POST body size — must be >= upload_max_filesize
post_max_size = 64M

; Maximum seconds a script may run
max_execution_time = 120

; ALWAYS set this — avoids per-call timezone warnings
date.timezone = "Asia/Kolkata"

; Production: do NOT show errors to visitors; log them instead
display_errors = Off
log_errors = On

After editing the FPM php.ini, restart the FPM service (a reload of the web server is not enough — PHP runs in the FPM process):

sudo systemctl restart php-fpm        # RHEL/AlmaLinux
# sudo systemctl restart php8.2-fpm   # Debian

Test with a phpinfo() page

Create a temporary info page in your document root:

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

If you created a new file under a custom root on RHEL/AlmaLinux, re-apply the SELinux label:

sudo restorecon -Rv /var/www/example.com

Visit http://example.com/info.php. You should see the PHP version table (not the raw source code — if you see source, PHP is not wired up). Check it with curl:

curl -s -H 'Host: example.com' http://localhost/info.php | grep -i 'PHP Version'

Delete the phpinfo page immediately after testing

phpinfo() exposes your full PHP configuration, loaded modules, paths, and environment — a goldmine for attackers. Remove it as soon as you have confirmed PHP works:

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

Verify your work

# 1. PHP-FPM is enabled and running
systemctl is-enabled php-fpm && systemctl is-active php-fpm

# 2. The FPM socket exists
ls -l /run/php-fpm/www.sock          # Debian: /run/php/php8.2-fpm.sock

# 3. Required extensions are loaded
php -m | grep -E 'mysqlnd|gd|mbstring'

# 4. A .php page renders (returns HTML, not raw PHP source)
curl -s -H 'Host: example.com' http://localhost/info.php | head -1

# 5. Your timezone is set (no warnings)
php -i | grep 'date.timezone'

Summary

  • LAMP = Apache + PHP; LEMP = nginx + PHP. Both run PHP as PHP-FPM today.
  • Install with dnf install php php-fpm php-mysqlnd php-gd php-mbstring ... (use dnf module list php to pick a version), then systemctl enable --now php-fpm.
  • nginx: a location ~ \.php$ block with include fastcgi_params;, fastcgi_pass to the FPM socket, and SCRIPT_FILENAME.
  • Apache: prefer mod_proxy_fcgi to PHP-FPM (works with the event MPM) over legacy mod_php.
  • Tune php.ini (memory_limit, upload_max_filesize/post_max_size, date.timezone) and restart php-fpm to apply.
  • Test with a phpinfo() page, then delete it. Watch SELinux on custom roots and for outbound connections.

Next: host multiple PHP sites in Virtual Hosts or secure them with HTTPS with Let's Encrypt.

Test yourself