How to configure PHP-FPM with NGINX on Ubuntu

Key Takeaways

- NGINX lacks built-in PHP support; PHP-FPM handles PHP requests via FastCGI over Unix sockets
- Create separate pools for each site to isolate resources and improve security
- Use dynamic process management with tuned pm.max_children to balance memory and performance
NGINX does not process PHP on its own. Unlike Apache with mod_php, NGINX serves static files directly and forwards PHP requests to an external process manager. PHP-FPM is that manager, and connecting the two over FastCGI is the standard setup for any PHP application running behind NGINX.
This guide walks through the complete configuration on Ubuntu, covering versions 22.04, 24.04, and 26.04 LTS. You will install PHP-FPM, create a dedicated pool for a site, wire NGINX to that pool via a Unix socket, and test the stack. The 502 Bad Gateway error, the most common failure mode, gets its own troubleshooting section.
What is PHP-FPM and why does NGINX need it?
PHP-FPM stands for FastCGI Process Manager. It is a PHP SAPI (Server API) that spawns a master process listening on a socket or TCP port. Worker processes sit ready to execute scripts. When NGINX receives a request for a .php file, it passes that request to PHP-FPM via the FastCGI protocol. PHP-FPM runs the script and returns the output. NGINX then sends the response to the client.
This architecture separates concerns cleanly. NGINX handles connections, SSL termination, and static assets. PHP-FPM handles PHP execution. Each can be tuned, scaled, or restarted independently.
Which PHP version ships with each Ubuntu release?
Ubuntu's default repositories include different PHP versions depending on the release. Ubuntu 22.04 LTS (Jammy) ships PHP 8.1. Ubuntu 24.04 LTS (Noble Numbat) ships PHP 8.3. Ubuntu 26.04 LTS (Resolute Raccoon) ships PHP 8.5. The commands in this guide use 22.04 as the baseline. Swap version numbers in package names, service names, and socket paths when working on newer releases.
Confirm your environment before editing any config:
lsb_release -rs
php -vStep 1: Install PHP-FPM
Update your package index and install the FPM package matching your PHP version:
sudo apt update
sudo apt install php8.1-fpmOn Ubuntu 24.04 and 26.04, you can also run sudo apt install php-fpm to install the default metapackage for that release.
Enable and start the service:
sudo systemctl enable php8.1-fpm
sudo systemctl start php8.1-fpm
sudo systemctl status php8.1-fpmThe status output should show active (running). Verify the default socket exists:
Code sample: ls -l /run/php/php8.1-fpm.sock
On 24.04 the socket path is /run/php/php8.3-fpm.sock. On 26.04 it is /run/php/php8.5-fpm.sock.
Step 2: Create a dedicated PHP-FPM pool
The default pool file lives at /etc/php/8.1/fpm/pool.d/www.conf. For a single site, editing www.conf is fine. For multiple applications, create one pool per site. Separate pools let you assign different users, resource limits, and PHP settings to each app.
This example creates a pool named wordpress_site running under a dedicated user:
Code sample: sudo groupadd wordpress_user
sudo useradd -g wordpress_user -d /var/www/wordpress -s /usr/sbin/nologin wordpress_user
Create the pool configuration file:
Code sample: sudo nano /etc/php/8.1/fpm/pool.d/wordpress_pool.conf
Add the following configuration:
Code sample: [wordpress_site]
user = wordpress_user
group = wordpress_user
listen = /run/php/php8.1-fpm-wordpress.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500
The pm = dynamic setting starts a base number of workers and scales between min_spare_servers and max_spare_servers based on load. pm.max_children caps total workers to prevent memory exhaustion. pm.max_requests recycles workers after 500 requests to avoid memory leaks.
Reload PHP-FPM to apply changes:
Code sample: sudo systemctl reload php8.1-fpm
Confirm the new socket was created:
Code sample: ls -l /run/php/php8.1-fpm-wordpress.sock
Step 3: Configure NGINX to use the pool
Create or edit a server block in /etc/nginx/sites-available/. The fastcgi_pass directive points to your pool socket:
Code sample: server {
listen 80;
server_name example.com;
root /var/www/wordpress;
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.1-fpm-wordpress.sock;
}
location ~ /\.ht {
deny all;
}
}
Enable the site and test the configuration:
Code sample: sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 4: Test the PHP-FPM and NGINX stack
Create a PHP info file in your document root:
Code sample: echo '<?php phpinfo();' | sudo tee /var/www/wordpress/info.php
Open http://example.com/info.php in a browser. You should see the PHP information page listing your PHP version, loaded modules, and server variables. Delete the file after testing. Leaving phpinfo() exposed is a security risk.
Unix socket vs TCP: which should you use?
The fastcgi_pass directive accepts either a Unix socket or a TCP address. Sockets are faster because they skip the network stack. Use them when NGINX and PHP-FPM run on the same machine. TCP (e.g., 127.0.0.1:9000) is necessary when PHP-FPM runs on a separate server or inside a container with network-based communication.
How to fix 502 Bad Gateway errors
A 502 error means NGINX cannot reach PHP-FPM. Check these in order:
- Is PHP-FPM running? Run sudo systemctl status php8.1-fpm.
- Does the socket exist? Run ls -l /run/php/php8.1-fpm-wordpress.sock.
- Do NGINX and the socket have matching permissions? The socket's listen.owner and listen.group should match the user NGINX runs as (usually www-data).
- Does the fastcgi_pass path in NGINX match the listen path in the pool config?
- Check the PHP-FPM log at /var/log/php8.1-fpm.log and NGINX error log at /var/log/nginx/error.log for specifics.
Most 502s come down to a typo in the socket path or a permission mismatch. When in doubt, reload both services after any config change.
Tuning PHP-FPM for performance
The pm.max_children value is critical. Too low and requests queue. Too high and the server runs out of memory. A rough formula: take your available RAM, subtract memory for NGINX, MySQL, and the OS, then divide by the average memory each PHP worker uses. On a 2GB droplet running WordPress, 10 to 15 children is a reasonable starting point.
Monitor process memory with ps aux --sort=-%mem | grep php-fpm. Adjust pm.max_children based on observed usage under real traffic.
Logicity's Take
PHP-FPM configuration is one of those tasks that looks trivial until it isn't. The pool-per-site pattern deserves wider adoption. It isolates failures, simplifies log analysis, and lets you apply different security restrictions (like disabling exec) to untrusted applications without affecting others. For teams running multiple PHP apps on a single server, separate pools are not optional.
Frequently Asked Questions
Can I run multiple PHP versions with PHP-FPM and NGINX?
Yes. Install each PHP-FPM version (e.g., php8.1-fpm and php8.3-fpm), create separate pools with different sockets, and point each NGINX server block to the appropriate socket.
What is the difference between pm static, dynamic, and ondemand?
Static keeps a fixed number of workers. Dynamic scales between min and max based on load. Ondemand spawns workers only when requests arrive, then terminates them after idle timeout. Dynamic is the default and suits most workloads.
Why use a Unix socket instead of TCP for fastcgi_pass?
Unix sockets are faster because they avoid TCP/IP overhead. Use TCP only when PHP-FPM runs on a different machine or in a network-isolated container.
How do I check if PHP-FPM is processing requests?
Enable the status page by adding pm.status_path = /status to your pool config. Then configure NGINX to serve that path and visit it in a browser or with curl.
Another practical tools guide for developers managing complex workflows
Need Help Implementing This?
Logicity's team can help you optimize your PHP stack, configure high-availability pools, or troubleshoot persistent 502 errors. Reach out via our contact page for a consultation.
Manaal Khan
Tech & Innovation Writer
Related Articles
Browse all
Google Workspace API Updates March 2026: New Calendar API, Chat Authentication, and Maps Changes
Google just dropped Episode 29 of their Workspace Developer News, and there's a lot to unpack. From a brand new secondary calendar lifecycle API to deprecation warnings for Apps Script authentication, here's everything developers need to know about the March 2026 platform updates.

Zig for Legacy C Code: How to Modernize Infrastructure Without a Risky Full Rewrite
A new blueprint from Zeba Academy shows developers how to surgically replace fragile C components with Zig modules. Instead of risky full rewrites, this approach lets you swap out problematic code piece by piece while keeping your battle-tested infrastructure intact.

Claude Skills vs Commands: When to Use Each for AI-Powered Coding Workflows
Claude's Skills and Commands look similar on the surface since both use markdown files, but they work completely differently. Skills run automatically based on context while Commands need explicit /invocation. Here's how to pick the right one for your coding workflow.

DualClip macOS Clipboard Manager: The Only Tool That Uses Dedicated Slots Instead of History
DualClip v1.2.6 just dropped with a major stability fix and Homebrew support. After analyzing 57 clipboard managers, the developer found every single one uses history. DualClip takes a radically different approach with three fixed slots and zero disk storage.


