502 After Deploy: Fix a PHP-FPM Listener That Nginx Cannot Reach
A running PHP-FPM container can still be unreachable from Nginx. Check the listener address and the caller's network before changing application code.
Check which container owns the listener
If Nginx starts returning 502 immediately after a Docker deployment, read its error log before restarting the stack. A useful first distinction is whether Nginx failed to connect to PHP-FPM or connected and then received a broken response. This guide investigates one reproducible connection failure: PHP-FPM listens on 127.0.0.1:9000 inside its own container while Nginx runs in another container.
In that arrangement, “PHP-FPM is running” and “Nginx can reach PHP-FPM” are separate claims. The address 127.0.0.1 belongs to the current network namespace. A connection from the Nginx container to its own loopback does not enter the PHP-FPM container. Resolving the PHP-FPM service name gives its container network address, which also cannot reach a listener bound exclusively to that container's loopback. Compose service discovery does not change the listener's binding. Docker networking documentation (opens in a new tab).
Read the upstream connection error
Run these commands on the Docker host, from the directory containing the relevant Compose file. The examples call the Nginx service web and the PHP service php-fpm; substitute your actual names.
docker compose ps --all
docker compose logs --tail=50 web
ps --all includes stopped containers. An exited PHP service should send you to its startup logs first. A running service is a reason to inspect the listener, not proof that the application is ready. Compose ps reference (opens in a new tab).
For the loopback-binding failure, the relevant Nginx error resembles:
connect() failed (111: Connection refused) while connecting to upstream
That message establishes a failed connection attempt to the configured upstream. Check the address and port recorded in the actual log. A wrong port or wrong destination can produce the same symptom; the error alone does not prove a loopback binding.
Use container logs when the image forwards its logs to stdout and stderr. If your deployment writes a regular file instead, read the configured file. Do not assume that tail on a guessed path will recover container log history.
Compare the two configurations
For separate containers sharing an intended private Compose network, a TCP configuration can look like this in the PHP-FPM pool:
[www]
listen = 0.0.0.0:9000
The matching Nginx directive points to the Compose service name and container port:
fastcgi_pass php-fpm:9000;
These settings solve different parts of the path: PHP-FPM accepts traffic on its container interfaces, and Nginx uses the service's network address. The services still need a shared network. Keep FastCGI private; this arrangement does not require publishing port 9000 on the host. If you deliberately share a network namespace, a loopback listener may be appropriate instead. PHP-FPM configuration (opens in a new tab).
To inspect the live TCP listener when ss is installed, run:
docker compose exec php-fpm ss -ltn
A LISTEN entry on 127.0.0.1:9000 confirms the loopback-only binding in that namespace. An entry on 0.0.0.0:9000 is compatible with cross-container IPv4 access, but does not by itself prove DNS, routing, access rules or FastCGI behavior. If ss is unavailable, inspect the pool configuration and startup logs; absence of the tool is not evidence of an outage.
Do not send an HTTP request with curl to port 9000. PHP-FPM expects FastCGI there. Verify the final HTTP response through Nginx after correcting the listener.
Unix sockets use different configuration syntax
If the deployment uses a shared Unix socket, the PHP-FPM pool takes a filesystem path:
listen = /run/php/php-fpm.sock
Nginx uses the unix: prefix:
fastcgi_pass unix:/run/php/php-fpm.sock;
The prefix belongs to Nginx's directive, not PHP-FPM's listen value. Separate containers must share the socket through an appropriate volume, and the Nginx worker needs access to the socket and its parent directories. A socket file's existence alone does not prove that a live process accepts connections. Nginx FastCGI reference (opens in a new tab).
Apply the change and account for service recreation
Change the source pool configuration used to build or mount the container. Editing a disposable container by hand is easy to lose at the next deployment. Deploy the updated PHP-FPM instance using the release procedure appropriate to your stack.
Recreating a Compose service can change its container IP. If Nginx resolved a static upstream address when it loaded its configuration, refreshing PHP-FPM alone may leave Nginx using the old address. Once the new PHP-FPM instance is listening, validate and gracefully reload Nginx so the service name is resolved for the updated configuration:
docker compose exec web nginx -t
docker compose exec web nginx -s reload
Run the reload only if the configuration test succeeds. A successful syntax check does not establish upstream availability. Recheck the error stream and make a request through the web server after the reload.
For a local web port published as 127.0.0.1:8080, a bounded HTTP probe is:
curl -sS --connect-timeout 2 --max-time 5 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
Replace the address with the entry point you actually expose. A 200 response from a known PHP route confirms that this request passed through the web server and reached a working handler. Check a known response body as well when a static fallback page could otherwise look successful. A curl error or status 000 requires examining its diagnostic output; it does not uniquely identify PHP-FPM as the cause.
Keep the experiment narrower than the production claim
A minimal two-container lab can isolate the binding issue without a database or application framework. Configure PHP-FPM to listen on loopback, request a PHP page through the other container and observe 502 plus an upstream connection refusal. Change the pool binding to the intended interface, recreate PHP-FPM, refresh Nginx's upstream resolution and verify a 200 response with the expected PHP-generated body.
That experiment tests this network path and fix. It does not establish that every post-deploy 502 has this cause or that a replacement deployment has no downtime. Application crashes, incorrect script paths and intermediary proxies require their own evidence.
For production releases, start and verify a candidate before switching traffic, then drain the old instance. Check both the public entry point and the application behavior you depend on. External availability monitoring can provide an additional signal after the switch, but a scheduled probe can miss a short outage. Keep the release gate and the post-release observation as separate checks.
Catch problems after your next deployment
Monitor availability, browser JavaScript errors and visual changes with DeployWatchers.