devkult_
tools29converters34
home/blog/fix-502-bad-gateway-nginx
nginxdebuggingdevops

How to Fix 502 Bad Gateway in nginx, Step by Step

A 502 Bad Gateway from nginx always means the same thing: nginx is fine, and whatever it forwards requests to — Node, PHP-FPM, Gunicorn, a container — didn't give it a usable answer. The good news is that nginx writes down exactly what went wrong. The fix starts in its error log.

Step 1: read the error log

sudo tail -n 50 /var/log/nginx/error.log

Reload the failing page while you watch, and match the message to its cause:

Message in error.log Usual cause
connect() failed (111: Connection refused) while connecting to upstream The app isn't running, or isn't listening where proxy_pass points
connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory) PHP-FPM is stopped, or the socket path or version is wrong
connect() to unix:… failed (13: Permission denied) nginx can't open the socket file, or SELinux blocks the connection
upstream prematurely closed connection while reading response header The app crashed or was killed mid-request
upstream sent too big header while reading response header Response headers (often cookies) exceed nginx's buffers
no live upstreams while connecting to upstream Every server in the upstream block is marked as failed

Step 2: talk to the app directly

From the server itself, skip nginx and call the app on its own port:

curl -i http://127.0.0.1:3000/

If this fails too, nginx was right: fix the app first. If it works, the problem is the connection between nginx and the app.

Fix: the app isn't running

sudo systemctl status myapp
sudo journalctl -u myapp -n 100 --no-pager

For Docker, docker ps and docker logs <container>. Look for a startup crash — a missing environment variable or a port already in use are the classics.

Fix: nginx points at the wrong place

Compare proxy_pass with what the app actually listens on:

sudo ss -ltnp | grep -E ':3000|:8000'

One subtle trap: proxy_pass http://localhost:3000 makes nginx try both IPv6 (::1) and IPv4 (127.0.0.1). If your app only listens on IPv4, some requests fail with connection refused on [::1]:3000 — intermittent 502s that are maddening to track down. Write the address explicitly:

proxy_pass http://127.0.0.1:3000;

Inside Docker Compose, 127.0.0.1 is the nginx container itself — use the service name instead (proxy_pass http://app:3000;).

Fix: PHP-FPM socket problems

The socket path includes the PHP version, and upgrades change it. List what exists:

ls /run/php/

Then point nginx at the real file:

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

For permission denied, check the pool config (/etc/php/8.3/fpm/pool.d/www.conf): listen.owner and listen.group should be the user nginx runs as (often www-data), with listen.mode = 0660 — owner and group can read and write, everyone else can't (what 660 means).

Fix: the app crashes mid-request

Upstream prematurely closed connection means the process died or hung up while handling the request. Check the app's logs for the failing request, and check whether the kernel killed it for using too much memory:

sudo dmesg -T | grep -i -E 'killed process|out of memory'

Fix: headers too big

Large cookies or long auth tokens can overflow nginx's default buffers:

proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;

For PHP, the equivalents are fastcgi_buffer_size and fastcgi_buffers.

Fix: SELinux (RHEL, CentOS, Fedora)

If the log says permission denied while connecting to a TCP port, SELinux may be stopping nginx from making network connections:

sudo setsebool -P httpd_can_network_connect 1

Intermittent 502s under load

If 502s come and go, suspect keep-alive. When nginx keeps connections to the app open (an upstream block with keepalive), it can reuse a connection at the exact moment the app closes it for being idle. Node.js closes idle connections after 5 seconds by default, so make the app's timeout longer than nginx's:

server.keepAliveTimeout = 65_000;   // longer than nginx's upstream keepalive_timeout (60s)
server.headersTimeout = 66_000;

After every change

sudo nginx -t && sudo systemctl reload nginx

nginx -t catches syntax errors before they take the site down. If the 502 has become a 504 instead, nginx now reaches the app but it's too slow — see 502 vs 503 vs 504 for that side of the story. And to recheck a batch of URLs once you're done, paste them into the HTTP Status Checker.

More from the blog