Fix Nginx 502 Bad Gateway behind a reverse proxy
Updated 4 November 2026 · By Brent Alexis
Short answer: a 502 Bad Gateway from Nginx means Nginx itself is working, but it couldn't get a valid answer from your app (the "upstream"). The error log says exactly why. Run sudo tail -n 20 /var/log/nginx/error.log, find the line from the time of the 502, and match it to the table below.
Every cause except SELinux was reproduced and fixed in the lab with ProxyForge's own reverse-proxy and PHP-FPM output: on the official nginx Docker image (Nginx 1.30.6) with a test app built to fail in each way, and on Debian 12's packages (Nginx 1.22.1, PHP 8.2.35) for the PHP-FPM cases: 37 checks. SELinux can't run inside containers, so that section isn't lab-tested. How it's tested
Match your error log line
| The error log says | Cause |
|---|---|
connect() failed (111: Connection refused) while connecting to upstream | Nothing is listening there, or the app only listens on 127.0.0.1 |
upstream prematurely closed connection while reading response header from upstream | The app crashed or closed the connection |
upstream sent too big header while reading response header from upstream | Response headers too big |
SSL_do_handshake() failed … while SSL handshaking to upstream | proxy_pass https:// to an HTTP app |
connect() to unix:/run/php/…sock failed (2: No such file or directory) | Wrong PHP-FPM socket, or PHP-FPM stopped |
… sock failed (13: Permission denied) | Nginx can't open the PHP-FPM socket |
connect() to 127.0.0.1:3000 failed (13: Permission denied) on RHEL, Alma or Rocky | SELinux |
upstream timed out (110: Connection timed out), with a 504 instead of a 502 | The app is too slow |
No new line at all? Make sure you're looking at the right server and the right log: in the official Docker image the log goes to docker logs, and a site can set its own error_log path.
Nothing is listening on the backend port
[error] 79#79: *3 connect() failed (111: Connection refused) while connecting to upstream
The app isn't running, crashed, or listens on a different port than the one in proxy_pass. Check from the server itself:
curl -I http://127.0.0.1:3000
sudo ss -tlnp | grep :3000
Use the port from your proxy_pass line. If curl can't connect and ss shows nothing, start the app (and check its own logs for why it stopped). If ss shows it on another port, fix one or the other. Checked: with nothing listening on the port, Nginx answered 502 with this line.
The app only listens on 127.0.0.1
Same log line, but the app is running. If Nginx and the app are in different containers or on different machines, an app listening on 127.0.0.1 only accepts connections from inside its own container or machine. ss -tlnp shows the address it's bound to: 127.0.0.1:3000 is local only, 0.0.0.0:3000 or *:3000 accepts connections from anywhere.
Fix: make the app listen on 0.0.0.0 (for example --host 0.0.0.0, or HOST=0.0.0.0, depending on the framework), or run Nginx on the same machine and proxy to 127.0.0.1. With Docker, the app must listen on 0.0.0.0 inside its container, and Nginx on the host proxies to the published port. Checked: an app listening on 127.0.0.1 in its own container gave Nginx, in another container, the same 502 and the same Connection refused line, while the app answered fine from inside its own container.
The app crashed or closed the connection
[error] 164#164: *7 upstream prematurely closed connection while reading response header from upstream
Nginx connected and sent the request, but the app hung up without sending a response. Usually the app crashed on that request, ran out of memory, or hit an error before writing anything. Nginx's log can't say more than that, so look at the app's own log for the same moment. Checked with a test app that reads the request and closes the connection: 502 and this line.
Response headers too big
[error] 206#206: *9 upstream sent too big header while reading response header from upstream
The app sent response headers bigger than Nginx's buffer for them (4 KB or 8 KB by default, one memory page). Big cookies and login or session tokens are the usual reason. Add these two lines inside the location block, next to proxy_pass:
proxy_buffer_size 16k;
proxy_buffers 8 16k;
Checked: a 10 KB response header gave a 502 with the line above, and these two lines fixed it. Add both: with only proxy_buffer_size 16k;, nginx -t fails with "proxy_busy_buffers_size" must be less than the size of all "proxy_buffers" minus one buffer.
proxy_pass https:// to an app that only speaks HTTP
[error] 283#283: *13 SSL_do_handshake() failed (SSL: error:0A00010B:SSL routines::wrong version number) while SSL handshaking to upstream
Most apps behind Nginx speak plain HTTP on their local port, and Nginx handles HTTPS for visitors. If proxy_pass starts with https://, Nginx tries a TLS handshake with an app that doesn't do TLS, and it fails. Fix: use proxy_pass http://127.0.0.1:3000;. Checked: 502 with this line, while the same config with http:// worked.
Wrong PHP-FPM socket, or PHP-FPM stopped
[crit] 6829#6829: *3 connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory) while connecting to upstream
For a PHP site, the "upstream" is PHP-FPM. This line means there's no socket at that path: the fastcgi_pass line names a PHP version that isn't installed (a config copied from a guide for PHP 8.1 on a server with 8.2, say), or PHP-FPM isn't running. Check what exists and whether it's running:
ls /run/php/
sudo systemctl status php8.2-fpm
Then point fastcgi_pass at the socket that exists, or start PHP-FPM. On RHEL, Alma and Rocky the socket is /run/php-fpm/www.sock and the service is php-fpm. Checked on Debian 12: a php8.1-fpm.sock path on a server with PHP 8.2 gave this line, and so did stopping PHP-FPM, because its socket disappears when it stops.
Nginx can't open the PHP-FPM socket
[crit] 6879#6879: *7 connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied) while connecting to upstream
The socket exists, but the user Nginx runs as (www-data on Debian and Ubuntu, nginx on RHEL) isn't allowed to open it. This happens after changing the listen.owner, listen.group or listen.mode lines in the PHP-FPM pool file. On Debian the defaults are listen.owner = www-data and listen.group = www-data, in /etc/php/8.X/fpm/pool.d/www.conf. Put them back (or match Nginx's user), then restart PHP-FPM. Checked on Debian 12: with the socket owned by root:root and mode 0660, Nginx got this line, and setting the owner and group back to www-data fixed it.
SELinux on RHEL, AlmaLinux and Rocky
With SELinux enforcing, Nginx isn't allowed to connect to most ports by default (an app on port 3000, for example), so a reverse proxy can fail with (13: Permission denied) while connecting to upstream, even though curl to the app works from the same server. The fix is one command:
sudo setsebool -P httpd_can_network_connect 1
Check whether SELinux is involved with getenforce (it says Enforcing) and sudo ausearch -m avc -ts recent. This section isn't lab-tested: SELinux doesn't run inside containers. ProxyForge's reverse-proxy output includes this command as a comment for the same reason.
A 504 instead: the app is too slow
[error] 325#325: *15 upstream timed out (110: Connection timed out) while reading response header from upstream
A 504 Gateway Timeout means the app accepted the request but didn't answer in time. By default Nginx waits 60 seconds. In the lab, an app that took 75 seconds got a 504 after 61 seconds, with the line above. Find out why the app is slow first. If a few requests really need longer (a big report or an upload being processed), raise the limit only for that path, for example proxy_read_timeout 300s; in its own location.
After the fix
sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com/
If the 502 stays after a change to the Nginx config, check that the reload actually applied: see nginx -s reload said OK, but my change didn't apply. For complete, tested reverse-proxy and PHP configs, see Nginx config file examples.
Generate a reverse-proxy config →
How to use ProxyForge · FAQ: common errors · Guides · What's new · About