PHP-FPM Unix Socket Permissions - Let Nginx Reach PHP Without Opening the Door to Everyone
Nginx returns a 502 response, and the error log mentions a Unix socket used by PHP-FPM. It is tempting to make that socket writable by everyone and move on. But a 502 is only the visible boundary failure. The socket may not exist, Nginx and PHP-FPM may name different paths, a parent directory may block traversal, or the Nginx worker may lack permission to connect.
A useful repair begins by identifying which condition is actually present. This article develops that diagnosis for Nginx and PHP-FPM running on the same Linux host. It then shows how to put the fix in the PHP-FPM pool configuration, where it can survive socket recreation, rather than relying on a one-time chmod.
The names app, www-data, and /run/php/app.sock below are placeholders. Active users, groups, service names, binary names, and paths vary between systems and packages; inspect them before changing anything.
Start with the Boundary, Not the Browser Message
In this arrangement, PHP-FPM is the listener and Nginx is the client. The PHP-FPM pool creates a local endpoint with its listen directive. Nginx sends a request to that endpoint with fastcgi_pass. The PHP-FPM configuration manual accepts either an IP address and port or a Unix socket path for listen. The Nginx FastCGI module likewise accepts a Unix-domain socket:
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/app.sock;
}
Both sides must describe the same endpoint. Similar-looking names are still different paths. A configured /run/php/app.sock does not reach /run/php/app-fpm.sock, and a running PHP-FPM service does not prove that the particular pool or socket Nginx expects is available.
The first evidence should therefore be the Nginx error log around one controlled request. Preserve the exact operation, path, and operating-system error. “No such file or directory,” “connection refused,” and “permission denied” point toward different investigations. They should not all be collapsed into “PHP is down.”
Check Existence Before Editing Permissions
Before changing ownership or mode, confirm that the expected socket exists and that something is listening. These commands inspect state without modifying it:
systemctl list-units --type=service --all 'nginx.service' 'php*-fpm.service'
ss -xl
stat -Lc 'type=%F owner=%U group=%G mode=%a path=%n' /run/php/app.sock
The wildcard helps discover a versioned PHP-FPM unit, but unit naming still depends on the package. A failed stat is not a permission diagnosis. It first asks whether the path is wrong, the pool failed to start, its parent directory was unavailable, or a different service unit owns the listener.
Compare the active Nginx configuration with the active pool configuration, not with a tutorial or an unused sample file:
; PHP-FPM pool
listen = /run/php/app.sock
# Nginx PHP location
fastcgi_pass unix:/run/php/app.sock;
Nginx can print its loaded configuration with nginx -T, but that output may contain unrelated sensitive settings and should not be pasted into a public ticket without review. PHP-FPM configuration-test commands and binary names vary by package. Use the package's installed binary and documentation rather than guessing a versioned command.
Identify the Processes That Need Access
Configuration labels are less useful than effective process identities. On a typical master-worker setup, an Nginx master may start with elevated privileges while request-handling workers run as an unprivileged account. It is the worker that needs to connect. PHP-FPM also separates pool worker identity from the metadata applied to its listening socket.
A broad process listing can reveal those identities without assuming their names:
ps -eo user,group,pid,ppid,comm,args
Filter and read the relevant Nginx and PHP-FPM rows locally. Also inspect the Nginx user directive and the pool's user, group, and listen.* directives. The pool process user answers “which account executes PHP code?” The socket owner, group, and mode answer “which local processes may reach this listener?” Those questions are related, but they are not identical.
The Whole Path Must Be Traversable
A pathname socket participates in filesystem permission checks. The Linux unix(7) manual says that creating a pathname socket requires write and search permission on its directory. On Linux, connecting to a stream socket requires write permission on the socket itself. The client must also be able to traverse the path leading to it.
That last detail explains why a socket can appear correctly owned while the connection still fails. Inspect every component, not just the final line:
namei -l /run/php/app.sock
stat -Lc 'owner=%U group=%G mode=%a path=%n' /run /run/php /run/php/app.sock
namei may not be installed on every system, so the repeated stat is a portable-enough fallback for this small path. Directories need search permission for the Nginx worker along the route. Adding write permission to a parent directory merely to permit traversal is both unnecessary and broader than the problem requires.
Put the Persistent Permission in the Pool
PHP-FPM provides three directives for a Linux Unix socket: listen.owner, listen.group, and listen.mode. The manual documents a default mode of 0660, with owner and group otherwise based on the running user. Packaged configuration can set different explicit values, so the active file remains authoritative.
Suppose inspection shows that the application pool runs as app and the Nginx workers run with group www-data. A narrowly scoped pool configuration could be:
[app]
user = app
group = app
listen = /run/php/app.sock
listen.owner = app
listen.group = www-data
listen.mode = 0660
This example gives the owner and group read/write bits while giving no socket bits to other local accounts. On Linux, the group write bit is what permits a worker in www-data to connect. The exact owner and group should follow the identities discovered on the target host. A dedicated FastCGI-access group can be narrower when several web servers or pools coexist, but introducing one also requires managing supplementary group membership and restarting affected processes so they receive the new membership.
The upstream PHP-FPM pool template documents the same separation between pool worker identity and socket metadata. It also documents listen.acl_users and listen.acl_groups for systems with POSIX ACL support. When those ACL options are set, listen.owner and listen.group are ignored. ACLs can express access without sharing a group, but mixing both mechanisms without noticing that precedence makes diagnosis harder.
Why a Quick chmod Is Not the Repair
A command that changes the live socket can be a temporary experiment: if access starts working after a carefully chosen metadata change, that is evidence about the failure. It is not yet durable configuration. PHP-FPM creates the pathname socket when the pool binds its listener. When that endpoint is removed and recreated during service lifecycle events, manually applied metadata can disappear with the old filesystem object.
The durable source of truth is therefore the pool configuration, plus whatever mechanism the operating system or package uses to create and protect the parent directory. The latter varies: do not assume that a manually created directory under /run will persist across a reboot.
Mode 0666 is especially weak as a default answer. It grants every local account write access to the endpoint even though the known client is one web-server identity. That may make an error vanish while discarding the boundary that socket permissions were meant to provide. Conversely, mode 0600 works only when the connecting worker qualifies as the owner; “more restrictive” is not useful if it excludes the intended client.
Validate, Apply, and Test in Layers
After editing, validate each daemon's configuration with the command supplied by the installed package. Do not restart both services blindly: if a test fails, leave the working service in place and correct the syntax first.
Once validation passes, apply the PHP-FPM change so it recreates the socket, then reload or restart Nginx only if its configuration changed. Repeat the inspection:
stat -Lc 'type=%F owner=%U group=%G mode=%a path=%n' /run/php/app.sock
namei -l /run/php/app.sock
Finally, request a harmless PHP route through Nginx and inspect both the HTTP result and fresh logs. This end-to-end check matters because readable metadata does not prove that Nginx loaded the expected server block, PHP-FPM loaded the expected pool, or mandatory access-control policy allowed the connection.
A compact troubleshooting order is:
- Capture the exact Nginx error and socket path from one request.
- Confirm the listener exists and the PHP-FPM pool is running.
- Confirm Nginx and PHP-FPM name the same endpoint.
- Identify the effective Nginx worker user and groups.
- Inspect the socket and every parent directory.
- Change the pool's
listen.*settings, not only the live socket. - Validate configuration, apply it, re-inspect metadata, and test through Nginx.
Know Where This Baseline Stops
Discretionary filesystem permissions are only one layer. SELinux, AppArmor, a systemd sandbox, a chroot, or container boundaries can deny a connection even when numeric mode bits appear sufficient. This article does not recommend disabling those controls. If the path and identities are correct but access is still denied, inspect the relevant policy logs and service confinement rather than widening the socket mode repeatedly.
The Linux behavior is also not a universal Unix rule. Both the PHP documentation and unix(7) note that some BSD-derived systems treat pathname socket permissions differently. Finally, separate PHP-FPM pools can improve operational separation, but the PHP manual explicitly cautions that pools are not a complete security mechanism; resources such as one OPcache instance may still be shared.
Fix the Contract Between the Processes
A PHP-FPM socket is a small contract between two processes. PHP-FPM chooses where the endpoint exists and which metadata it receives. Nginx must name that same endpoint and connect under an identity that the path permits. A parent directory and any additional confinement layer also participate in the decision.
That model is more useful than treating every 502 as a request for broader permissions. Establish whether the endpoint exists, compare both configurations, inspect the real identities and full path, then encode the smallest working access in the pool. The goal is not merely to make the page load once; it is to make the intended connection understandable and reproducible after the socket is created again.
