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.