An nginx troubleshooting checklist that starts with the signal
Something is broken. A page shows the wrong thing, an error code, or nothing at all. Good nginx troubleshooting means reading the signal nginx is already giving you instead of guessing and reloading in circles. Almost every problem falls into a handful of buckets, and each bucket has a tell.
This lesson is that checklist. Start at the top every time.
First: is the config even valid and applied?
Before debugging behavior, rule out the two things that fool everyone.
sudo nginx -t
nginx -t parses your config without touching the running server, from testing config safely. It either says syntax is ok / test is successful, or it prints the exact file and line of the problem. Fix that line first, everything else is noise until it passes.
Then remember: editing a config file changes nothing until you reload.
sudo systemctl reload nginx
Reload re-reads the config on a running server, from start, stop, reload. If a change "did not work," nine times out of ten it was never loaded. Test, then reload, every time.
Read the error log, always
The error log is where nginx explains itself, from access and error logs. Watch it live while you reproduce the problem:
sudo tail -f /var/log/nginx/error.log
Then load the broken page in another window. The line that appears is usually the answer: a permission denied, a missing file with its full path, or a connection refused to a backend. Do not debug blind when the log is right there.
502 Bad Gateway: the backend is not answering
A 502 means nginx tried to reach a backend (PHP-FPM or your Node/upstream app) and got nothing usable.
Check, in order:
-
Is the backend running?
systemctl status php8.4-fpmfor PHP, or check your Node process. A stopped backend is the most common cause. -
Is the address right? The
fastcgi_passsocket path (from the fastcgi_pass block) or theproxy_passtarget (from proxy_pass) must match where the backend actually listens. A wrong socket path or port gives an instant 502. -
Permissions on the socket. For a Unix socket, the nginx user must be allowed to read it. The error log will say
permission deniedon the socket path.
The error log line for a 502 usually contains connect() failed or connect() to ... failed, which points straight at the address.
One 502 catches people on RHEL, CentOS, Rocky, and Fedora: the paths and permissions all look right, but the log says permission denied on a socket that is world-readable. That is SELinux blocking nginx from talking to the backend, not a file permission at all. setsebool -P httpd_can_network_connect 1 fixes it. On Debian and Ubuntu you will not hit this, which is exactly why it is so confusing when you move a working config to a Red Hat box.
504 Gateway Timeout: the backend is too slow
A 504 is different from a 502. Nginx did reach the backend, but the backend took too long to respond and nginx gave up.
- The backend is stuck on a slow query, an external API call, or a heavy report.
- Or
proxy_read_timeout/fastcgi_read_timeoutis set too low for a legitimately long request.
The real fix is usually to make the request faster or move the work to a background job, not to raise the timeout forever. But you can raise it if the request is genuinely long:
proxy_read_timeout 120s;
A tell for reading these: if the page dies at almost exactly 60 seconds, that is nginx's default timeout cutting the cord, not your app failing on its own. Raise the nginx timeout and you may then hit PHP's max_execution_time a bit later, or Node's own server timeout. The layers stack, so a real fix often means lifting more than one ceiling, which is another reason background jobs beat long requests.
403 Forbidden: nginx refuses to serve it
A 403 means nginx found the request but will not serve it. Three usual causes:
-
File permissions. The nginx user cannot read the file or traverse into the folder. The error log says
permission denied. Fix ownership and permissions so nginx can read the path. -
No index file. A request for a directory (like
/) with noindexfile and noautoindex onreturns 403, because nginx has nothing to show and will not list the folder. This is the behavior from autoindex. -
An explicit
deny. An access rule from access control is blocking the request, for example the dotfile rule that hides.env. Check whether you accidentally denied something legitimate.
404 Not Found: nginx looked in the wrong place
A 404 means nginx resolved a path and no file was there. This is almost always root or try_files.
-
Wrong
root. Ifrootpoints at the wrong folder, every path 404s. Print the real path nginx is building: it isroot+ the URI. Compare that to where the file actually is.rootis from root and index. -
try_filesfallback. Atry_files $uri $uri/ =404;returns 404 when nothing matches, which is correct for a static site but wrong for an app that should route unknown paths to a front controller. For Laravel the last argument is/index.php?$query_string, not=404. Both from try_files. -
Trailing slash or matching. The wrong
locationmay be handling the request. Review matching priority to confirm which block wins.
Config change did not apply
If behavior did not change after an edit:
-
You forgot to reload. Run
nginx -tthensystemctl reload nginx. This is the number-one cause. -
You edited a file nginx does not include. A file in
sites-availabledoes nothing until it is symlinked intosites-enabled(or included byconf.d), from includes and sites. Check that your file is actually pulled in. -
The browser cached it. Especially with
expires 1yon assets (from caching static assets), your browser may serve the old file. Hard-refresh or test withcurl -Ito see nginx's real response without the browser cache.
Redirect loop (ERR_TOO_MANY_REDIRECTS)
The browser bounces forever between two URLs. The cause is almost always HTTPS confusion behind a proxy.
-
The app does not know it is on HTTPS. Nginx terminates TLS and sends plain HTTP to the backend, so the app thinks the request is insecure, redirects to
https://, which hits nginx, which sends plain HTTP again, forever. The fix isproxy_set_header X-Forwarded-Proto $scheme;from proxy headers, plus telling your framework to trust it (Laravel'sTrustProxies, Express'strust proxy). -
A redirect pointing at itself. A
return 301(from redirects with return) whose target matches the same block loops. Make sure your HTTP-to-HTTPS redirect lives in the port-80 block only, not the 443 block.
Trace it with curl, which shows each hop:
curl -IL https://example.com
The 30-second checklist
Run this top to bottom before anything else:
sudo nginx -t # 1. is the config valid?
sudo systemctl reload nginx # 2. did you actually reload?
sudo tail -f /var/log/nginx/error.log # 3. what does nginx say?
curl -I https://example.com # 4. what does nginx really return?
systemctl status php8.4-fpm # 5. is the backend alive?
Most problems reveal themselves in these five commands. The error log (step 3) is the single most useful one; keep it open while you reproduce the issue.
FAQ
502 or 504, what is the difference?
A 502 means nginx could not get a valid response from the backend at all (it is down or the address is wrong). A 504 means nginx reached the backend but it did not answer in time (it is too slow). Down versus slow.
The site works but my config change did nothing.
You almost certainly did not reload, or you edited a file that is not included. Run nginx -t then systemctl reload nginx, and confirm your file is symlinked into sites-enabled or covered by an include.
How do I see nginx's real response without my browser caching it?
Use curl -I https://example.com. It sends one request and prints only the response headers, with no cache. Add -L to follow redirects and reveal loops.
Where is the error log if the default path is empty?
Check the error_log directive in your nginx.conf and in the specific server block; a site can log to its own file. If nginx failed to start at all, the reason is usually printed by nginx -t or journalctl -u nginx instead.