#

Web Stack

Subscribe
Web Stack October 29, 2026·11 min read

Configure Nginx and PHP-FPM Safely

If Nginx and PHP-FPM run on the same Linux host, point Nginx at the pool's private Unix socket, map SCRIPT_FILENAME to the PHP file you want to run, and check that the response contains PHP-generated output. If they run in separate containers or on different hosts, use a reachable private FastCGI endpoint and confirm the script path from PHP-FPM's side. Request a nonexistent PHP file before sending real traffic to the site.

A PHP website can serve its HTML, CSS, and images perfectly while every PHP request fails.

Three things have to be true: Nginx reaches the PHP-FPM listener, PHP-FPM gets a valid script filename, and the response contains output PHP generated.

What Nginx handles and what PHP-FPM runs

Nginx PHP architecture

Nginx accepts HTTP connections, matches incoming requests to server and location rules, and serves static files. For requests configured to use PHP, it forwards FastCGI parameters to PHP-FPM. A PHP-FPM worker executes the named script and returns its output through Nginx.

A developer deploying a PHP website on a VPS wrote on r/PHP: “I want to deploy my PHP website on my VPS and thought it would be simpler.” PHP-FPM is the extra piece. It manages the PHP workers, while Nginx keeps handling the client-facing connection.

Browser
   |
   | HTTP request: /index.php
   v
Nginx
   |
   | FastCGI request over private socket or TCP
   v
PHP-FPM pool
   |
   | Execute the specified script
   v
PHP application
   |
   | Generated response
   v
PHP-FPM -> Nginx -> Browser

FastCGI needs a reachable listener and request parameters that name the right PHP script. A static index.html response only tests Nginx's file handling. A request for index.php tests the FastCGI path too.

The php command in your terminal runs PHP in command-line mode. Nginx contacts the configured PHP-FPM service and pool instead.

Find the PHP-FPM pool and listener your server uses

PHP FPM listener discovery

Find the running PHP-FPM service and its pool's listen setting before changing Nginx. Setup guides use example versions, pool names, and socket paths, and yours can differ.

On an Ubuntu or Debian-style server managed by systemd, list matching PHP-FPM service units:

sudo systemctl list-units --type=service --all 'php*-fpm.service'

Then check which listeners are configured:

sudo grep -R -nE \
  '^[[:space:]]*listen[[:space:]]*=' \
  /etc/php/*/fpm/pool.d/

Several results point to several PHP versions or custom pools. Work out which pool serves your site.

sudo ls -l /run/php/

Run that last command only if /run/php/ exists. Compare the entries with the pool's listen value and the running service. A pool can contain listen = /run/php/php8.3-fpm.sock.

Ubuntu's package registry associates the default php-fpm package in Ubuntu 24.04 LTS with PHP 8.3-FPM and the default package in Ubuntu 26.04 with PHP 8.5-FPM. Your host can run another PHP version or a custom pool, so go by what your own configuration shows.

Nginx configuration inspection

To see the configuration Nginx loads, including server blocks and included files, run:

sudo nginx -T

Review that output locally, because it can contain site-specific settings. Find your hostname, root directory, PHP location, and FastCGI include.

SettingRecord from the server
Nginx server blockThe block that handles your hostname
Document rootThe root path in effect
PHP-FPM serviceThe running service and PHP version
PHP-FPM poolThe pool file and its worker settings
Pool listenerThe exact socket path or private TCP address
Script locationThe path PHP-FPM can access

Choose a Unix socket or a private TCP connection

FastCGI transport comparison

A permission-controlled Unix socket fits Nginx and PHP-FPM on one Linux host, as long as both processes can access it. Separate containers or hosts can use a private TCP endpoint or a deliberately shared Unix socket.

You name the endpoint in Nginx with fastcgi_pass:

fastcgi_pass unix:/run/php/php8.3-fpm.sock;

If the pool listens on TCP, the same directive takes an address and port:

fastcgi_pass 127.0.0.1:9000;
CriterionUnix socketPrivate TCP
Typical arrangementBoth services share a Linux hostServices use separate network environments
EndpointFilesystem socket pathIP address or private hostname and port
Access controlSocket permissions and applicable ACLsBinding, permitted clients and network policy
Common failureSocket missing or inaccessibleAddress wrong, connection refused or network blocked
Suitable deploymentShared socket namespacePrivate interservice network
Container FastCGI connections

Inside an Nginx container, 127.0.0.1 is that container's own loopback interface. A separate PHP-FPM container needs its own reachable service endpoint or a shared Unix socket.

Map SCRIPT_FILENAME to the intended PHP file

PHP script path mapping

Once Nginx reaches PHP-FPM, the backend must receive a SCRIPT_FILENAME that resolves to your script in PHP-FPM's filesystem.

For a basic site with this document root:

root /var/www/php-example/public;

and a request for /_pg061_probe.php, the path should be:

/var/www/php-example/public/_pg061_probe.php

In separate containers, the same application files can be mounted under different paths. The absolute filename sent through FastCGI must point to a file the PHP-FPM container can see.

With try_files $uri =404, Nginx checks for the requested file under its own root or alias, and PHP-FPM separately resolves the SCRIPT_FILENAME it receives. If the file isn't on Nginx's filesystem, the sample file-existence rule needs an alternative for your deployment.

Check the FastCGI parameters

Nginx passes request fields to the backend with fastcgi_param. A packaged include such as fastcgi.conf can already provide SCRIPT_FILENAME:

include fastcgi.conf;

Check what the include actually sets, and define the filename in one place only. The Nginx FastCGI module reference documents an important inheritance rule: parent-level fastcgi_param directives are inherited only when the current level contains none.

PHP filename resolution

Reject missing PHP files

A PHP location that serves physical files can check for the script before it sends the request to PHP-FPM:

try_files $uri =404;

With this layout, a missing PHP file returns 404. WordPress and other front-controller applications need their own URL routing, configured separately.

A Primary script unknown message points at the filename PHP-FPM received.

Configure the Nginx PHP location

Nginx PHP configuration

The following server block shows Ubuntu 24.04 with Nginx and PHP 8.3-FPM on the same host, serving a simple site with physical PHP files. Replace the hostname, root, listener and include values with the ones you recorded earlier.

server {
    listen 80;
    server_name php.example.test;

    root /var/www/php-example/public;
    index index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        try_files $uri =404;

        include fastcgi.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

On Ubuntu installations that use /etc/nginx/sites-available/ and /etc/nginx/sites-enabled/, save a new site configuration under sites-available and enable it with a symlink in sites-enabled. If the hostname already has an active server block, edit that block instead of adding a second one for the same hostname.

The PHP location checks that the requested file exists and forwards it through the pool's Unix socket. It assumes your installed fastcgi.conf supplies the correct FastCGI parameters, including SCRIPT_FILENAME.

With this index setting, a directory request that selects index.php triggers an internal redirect and a new location match, so the PHP handler processes that file.

FastCGI location directives

Production applications can need protected-path rules and upload restrictions defined by the application. This small server block is not a complete production security policy.

Restrict PHP-FPM access and script execution

PHP FPM access security

Security-critical: The PHP-FPM configuration manual warns that exposing FastCGI on a publicly reachable address can permit arbitrary code execution. Do not expose a PHP-FPM listener directly to the public Internet.

Unix socket ownership and ACLs

PHP-FPM pools provide listen.owner, listen.group and listen.mode to control Unix socket access on Linux. The Nginx worker needs access to the socket and to every parent directory on its path.

On systems with POSIX ACL support, listen.acl_users and listen.acl_groups can define socket access. When you configure them, PHP-FPM ignores listen.owner and listen.group. Check the ACLs before changing the ownership directives.

Don't use chmod 777 on the socket to fix a Permission denied error. It's unsafe.

TCP listener restrictions

For TCP, listen.allowed_clients limits which client IP addresses can connect. PHP-FPM documents the unset value as allowing any client address that can reach the listener, so private binding and network controls are essential.

Socket permission controls

Keep TCP FastCGI inside the trusted service network, behind firewall and listener restrictions. Don't publish a container's FastCGI port just to connect two internal services.

Allowed script extensions

The pool directive security.limit_extensions restricts which main-script extensions PHP-FPM will run. A pool that serves only .php main scripts can use:

security.limit_extensions = .php

The PHP-FPM manual documents .php .phar as the default set. Use the narrower value if your application doesn't need the other extensions.

Verify PHP execution through Nginx

PHP execution verification

Run a known PHP script through your Nginx server block and read the response body. The HTTP status and Content-Type give context, but the body confirms that PHP produced the output.

First, test the Nginx syntax and the files it references:

sudo nginx -t

On the example PHP 8.3-FPM installation, you can run the matching FPM configuration test as:

sudo php-fpm8.3 -t

Use a temporary PHP output marker

On a test site you control, create _pg061_probe.php under the document root you verified, with these contents:

<?php
header('Content-Type: text/plain; charset=UTF-8');
echo 'PG061_FPM_OK';

The expected response body is exactly:

PG061_FPM_OK

To request the sample HTTP virtual host on port 80 locally, use:

curl --resolve php.example.test:80:127.0.0.1 \
  http://php.example.test/_pg061_probe.php

The sample request assumes Nginx accepts HTTP on that local address and port. An HTTPS-only site needs a request that matches its own listener and hostname.

PHP response acceptance

Reject a nonexistent PHP file

Ask for a script that doesn't exist, through the same virtual host:

curl -i --resolve php.example.test:80:127.0.0.1 \
  http://php.example.test/_pg061_absent.php

With the sample try_files $uri =404 rule, you should get HTTP 404.

CheckExpected resultIf it fails, check
Nginx configuration testSyntax test succeedsDirectives, includes, referenced files
PHP-FPM serviceYour pool is running. Listener availableService and pool configuration
Known PHP markerResponse body equals PG061_FPM_OKListener, routing, script mapping
Nonexistent PHP scriptHTTP 404 for sample ruleLocation match and file-existence handling
PHP source exposureNo raw PHP source in responseServer and location selection
A real app URLExpected application responseApplication routing, once plain PHP works

If the known marker fails, use the response and the error log to find the matching case in the next section. The 404 test only proves the sample location rejects missing files. Your application's own routes need separate checks.

Remove the probe after testing. You don't need a public phpinfo() page for this check, and one left accessible can expose server details.

Diagnose FastCGI errors by their symptoms

Nginx PHP errors

Start with the error logged for the failing request, then look at the PHP-FPM service or pool the log points to.

SymptomWhere to lookFirst check
Static HTML works, PHP returns 502Dynamic upstream connectionFastCGI listener, FPM service and logs
No such file or directory while connectingUnix socket pathConfigured listen and socket existence
Connection refusedBackend acceptanceService state and intended endpoint
Permission denied on socketSocket authorizationWorker identity, ACL, ownership and directory access
Primary script unknownScript resolutionSCRIPT_FILENAME and FPM-visible path
Literal PHP source in responseRequest routingServer/location match and PHP forwarding

Missing or unreachable backend

Nginx can keep pointing at a socket name after the PHP-FPM service changes or fails. Compare the socket in your Nginx configuration with your FPM pool's listen setting before changing either service.

A TerraMaster NAS operator on r/HomeNAS wrote: “The nginx config expects PHP-FPM to be installed, but it's not.”

FastCGI failure diagnosis

Socket access denied

A Permission denied error on the socket means the Nginx worker can't open it. Check which user the worker runs as, then the socket's permissions and ACLs.

Primary script unknown

Check that the filename PHP-FPM receives exists in its own filesystem. It fails when container mount paths differ.

Raw PHP appears in the response

PHP source in the response is unsafe. Find which Nginx server and location served the file and take that endpoint out of public service until PHP handling is fixed.

Apply changes and keep a recovery path

Nginx configuration rollback

Before changing a live server block or PHP-FPM pool, save the known-good files and note which services use them. Make the smallest change that works, then run the Nginx and FPM configuration checks. On the Ubuntu systemd setup shown here, reload Nginx with sudo systemctl reload nginx and apply pool changes through the FPM service. Repeat the probe requests and one real app URL. If anything fails, restore the saved files and confirm the old routes respond.

Identify cases that need application-specific rules

PHP deployment environments

The FastCGI connection proves PHP runs for the route you tested. Applications can need more:

  • WordPress: Sensitive-file and upload handling need their own rules. Use the application's Nginx setup guidance for them.
  • Apache migrations: Nginx doesn't read .htaccess. Translate the rewrite rules into explicit Nginx configuration.
  • Multi-site hosting: Separate PHP-FPM pools give you operational separation, but a dedicated pool alone doesn't isolate tenants completely.
  • Capacity tuning: pm.max_children limits concurrent PHP-FPM requests per pool. Set it from your observed memory use and workload, not from a copied worker count.
NoScript Team