Start with these four commands
Do not start editing config. Run these in order and the cause usually surfaces on its own:
# 1. What is Nginx actually complaining about?
tail -f /var/log/nginx/error.log
# 2. Is the upstream alive? On the port you think?
ss -lntp | grep 3000
# 3. Bypass Nginx entirely and hit the upstream directly
curl -i http://127.0.0.1:3000/
# 4. Is the config even valid?
nginx -t
Step 3 is the decisive one. If curl against the upstream also fails, Nginx is not your problem — your application is. That single check eliminates half the search space.
The six causes of 502
The upstream is not running. error.log says connect() failed (111: Connection refused). Errno 111 means nothing is listening on that port. Confirm the process is up and the port is right.
The upstream binds only to 127.0.0.1 while Nginx lives elsewhere. Many frameworks bind to loopback by default, so nothing outside the machine can reach them. Bind to 0.0.0.0 instead.
Proxying to 127.0.0.1 from inside Docker. In a container, 127.0.0.1 is that container. Within a compose network, use the service name:
# Wrong: points at the Nginx container itself
proxy_pass http://127.0.0.1:3000;
# Right: points at the service named "api"
proxy_pass http://api:3000;
Response headers too large. Usually caused by fat cookies or JWTs. error.log says upstream sent too big header. Nginx’s header buffer is small by default:
proxy_buffer_size 16k;
proxy_buffers 4 16k;
proxy_busy_buffers_size 32k;
The upstream crashed or was OOM-killed. The process dies the moment the request arrives, so Nginx never gets a complete response. Check your application’s own logs and dmesg | grep -i oom.
SELinux is blocking it. Only on CentOS, RHEL and Fedora. The symptom is maddening: the config is right, curl to the upstream works, but going through Nginx yields 502.
getenforce # only suspect if this says Enforcing
setsebool -P httpd_can_network_connect 1 # let Nginx open network connections
403 Forbidden
Almost always one of three things when serving static files:
- Directory permissions. The Nginx worker (typically the
www-dataornginxuser) needs read access to the site directory and execute permission on every directory along the path. A site under/root/will always 403. - No index file, and no directory listing. Requesting
/finds noindex.html, and Nginx refuses to list the directory. Either add an index file or setautoindex onin that location — noting that this publishes your file listing. - A wrong
rootpath. Check the concatenation:root /var/wwwwithlocation /static/resolves to/var/www/static/, not/var/www/. location matching rules covers this in detail.
413 Request Entity Too Large
Uploads fail and never reach the backend, because client_max_body_size defaults to 1m:
client_max_body_size 50m;
Reload afterwards. If a CDN or cloud load balancer sits in front of Nginx, it has its own limit that must be raised too.
504 Gateway Timeout
The connection succeeded, but the upstream is too slow. Nginx gives up after proxy_read_timeout (60 seconds by default):
proxy_connect_timeout 5s; # keep short so failures surface fast
proxy_read_timeout 300s; # how long to wait for upstream data
proxy_send_timeout 300s;
Raising the timeout treats the symptom. A 504 usually means some endpoint really is slow — a heavy query, a blocking third-party API — and converting it into an async job beats setting a five-minute timeout. WebSocket connections are the legitimate exception; see Nginx Config Examples.
The config change did nothing
Work through these in order:
- You forgot to reload.
nginx -tvalidates, it does not apply. Runnginx -s reload. - The request went to a different server block. When no
server_namematches, Nginx hands the request to the block markeddefault_server, or to the first one in the file. Verify precisely withcurl -H "Host: example.com" -I http://127.0.0.1. - Your browser cached a 301. Permanent redirects stick around for a long time, so you keep seeing old behavior on your own machine. Use a private window, or
curl -I. - You edited a file that is not loaded.
nginx -T(capital T) prints the complete config currently in effect, including everything pulled in byinclude. If your change is not in that output, that file is not being read.
Next steps
- Rather than hand-writing a config and then debugging it, generate a correct one with the Nginx Config Generator;
- To really understand location priority and the proxy_pass slash, see How to Write Nginx Config;
- For ready-made WebSocket, CORS and HTTPS snippets, see Nginx Config Examples.