SadServers
  • Scenarios
  • Labs
    All Labs Linux & Bash Web Servers Databases Data Processing Docker Kubernetes CI/CD Infrastructure as Code Tooling / Applications
  • Dashboard
  • Solutions
    For Individuals For Businesses
  • Ranking
  • Newsletter
  • Documentation
    FAQ Support Pro Accounts Pro+ Accounts Business Accounts Gift API CLI/TUI Privacy Troubleshooting Interviews
  • Blog
  • Pricing
  • Gift
    Gift Purchase Gift Redeem
  • About
Log In - Sign Up
  1. Labs
  2. Nginx
  3. Troubleshooting

Guide

Concepts and learning path

Troubleshooting

Failure modes and fixes

Cheatsheet

Commands to keep handy

Nginx troubleshooting

Nginx won't start or reload

Run nginx -t for the exact file and line of syntax errors. Check journalctl -u nginx -n 50. Common causes: duplicate listen directives, missing semicolons, or invalid paths in include files.

Reload succeeded but behavior didn't change

nginx -t and systemctl reload nginx both succeed, but the site still behaves like the old config. The running process may not be using the file you edited, or your request may never reach the block you changed.

Checks:

nginx -T | grep -n 'your-change' ps aux | grep nginx

nginx -T dumps the effective config the master loaded — not the file you think you edited. Confirm your change appears there. ps aux shows which binary and config path the running workers use; multiple Nginx instances or a container vs host install are easy to miss.

Common causes:

  • Editing the wrong config file — distro defaults under /etc/nginx/sites-available/ vs a custom nginx.conf; always verify with nginx -T
  • Config included from another file — your edit is overridden by a later include or a duplicate server block in conf.d/
  • Traffic hitting another server — load balancer, CDN, or wrong server_name block; see Wrong site or default page served below. Test locally: curl -v -H "Host: your.domain" http://127.0.0.1/
  • Stale container image — config baked into the image or a volume mount pointing elsewhere; reload inside the container does not pick up host-side edits

If nginx -T shows the new config but behavior is unchanged, the problem is upstream of Nginx (cache, proxy, DNS) or you are testing a different host than the one you reloaded.

403 Forbidden

File permissions, missing index file when autoindex off; is set, or no directory listing enabled. Nginx user (usually www-data or nginx) needs execute permission on every parent directory and read on the file. SELinux on RHEL may block access — check audit.log and: ausearch -m avc or sealert -a /var/log/audit/audit.log. See worker process permissions below — checks as root can be misleading.

Worker process permissions

The Nginx master process runs as root to bind privileged ports (80, 443). Worker processes — the ones that read files and proxy requests — run as www-data, nginx, or whatever user is set in nginx.conf.

File access errors are always the worker user's permissions, not root's. A common trap: you verify paths as root (cat /var/www/app/index.html works) but Nginx still returns 403 because the worker cannot traverse or read the path.

grep -E '^(user|group)' /etc/nginx/nginx.conf sudo -u www-data test -r /var/www/app/index.html && echo ok sudo -u www-data ls -la /var/www/app/

Test as the worker user, not root. Every directory from / to the file needs execute (x) for others or matching ownership/group. Unix socket upstreams use the same worker identity — see Unix socket backend above.

502 Bad Gateway / 504 Gateway Timeout

Upstream is down or not responding. Test from the Nginx host: curl -v http://127.0.0.1:8000.

Check error_log for "connect() failed" or timeout messages, most common ones are:
connect() failed (111: Connection refused)
upstream timed out
no live upstreams

Verify proxy_pass URL and that the backend listens on the expected socket.

Unix socket backend

When proxy_pass targets a Unix socket (proxy_pass http://unix:/run/app.sock), the socket file must exist, be writable by the Nginx worker user (often www-data or nginx), and the backend must be running. A missing or wrong-permission socket produces 502 with a different error_log message than a TCP connection failure — often "No such file or directory" or "Permission denied" on the socket path.

ls -la /run/app.sock grep -E '^(user|group)' /etc/nginx/nginx.conf systemctl status php8.2-fpm # or gunicorn, puma

Common with PHP-FPM, Gunicorn, and Puma. Fix socket path in both Nginx and the app config, align group membership or socket mode (0660), and ensure the upstream service starts before Nginx proxies to it.

Wrong site or default page served

The default server for a port handles requests with no matching server_name. Disable or fix the default site. Test which block matches: curl -v -H "Host: api.example.com" http://127.0.0.1/. See server_name matching order below for how Nginx picks a server block (if multiple server blocks match, Nginx selects the most specific name match. If no server block matches, Nginx selects the default server for that port).

server_name matching order and default server

Nginx chooses a server block for each request using a fixed priority on the Host header (and SNI on HTTPS):

  1. Exact match — server_name api.example.com;
  2. Leading wildcard — server_name *.example.com;
  3. Trailing wildcard — server_name example.*;
  4. Regex — server_name ~^api\.;
  5. Default server — first listen on that port with default_server, or the first block if none is marked

A common bug: a broad regex server_name unintentionally wins over a more specific block you expected to match. Another: traffic lands on the default server because the Host header does not match any name you defined.

nginx -T | grep -E 'server_name|listen|default_server' curl -v -H "Host: api.example.com" http://127.0.0.1/

Dump the effective config with nginx -T, list every server_name and listen on the port, then test with curl -H "Host: ...". Mark the intentional catch-all with listen 443 ssl default_server; so it does not steal traffic by load order alone.

413 Request Entity Too Large

Upload exceeds client_max_body_size (default 1m). Increase in the server or http block and reload.

Address already in use

Port 80 or 443 is held by Apache, Caddy, or another Nginx instance. Find the process: ss -tlnp | grep ':80 '. Remove duplicate listen directives or stop the conflicting service.

root vs alias confusion

root and alias both map URLs to filesystem paths, but they append the location prefix differently. Mixing them up — or using the wrong one — causes a huge number of mysterious 404s. Config tests pass; the path looks right when you ls the directory as root.

With root, Nginx appends the full request URI to the directive value:

location /images/ { root /data; } # GET /images/file.jpg → /data/images/file.jpg

With alias, Nginx replaces the matched location prefix with the alias path — the location segment is not repeated:

location /images/ { alias /data/; } # GET /images/file.jpg → /data/file.jpg

Common traps: using root /data/images; when files live directly under /data/ (you need alias /data/; or root /data; with location /images/). Trailing slashes matter — location /images without a trailing slash paired with alias is a frequent misconfiguration. alias inside regex locations needs a capture group.

nginx -T | grep -A5 'location /images' # Confirm the resolved path matches where the file actually is ls -la /data/file.jpg ls -la /data/images/file.jpg

Debug: dump the block with nginx -T, then check which filesystem path Nginx would use for a sample URI. See try_files misconfiguration below — fallback paths are resolved the same way.

try_files misconfiguration

A very common source of 404s and odd routing — try_files checks paths in order and uses the last argument as fallback. The standard static pattern:

try_files $uri $uri/ =404;

Common misconfigurations:

  • Wrong last argument — =404 when you meant SPA fallback (/index.html) or a named location (@backend)
  • Missing index — $uri/ looks for index.html via the index directive; without it, directory URLs 404 even when the file exists
  • Named location fallthrough — last arg @proxy must exist; typos or wrong location @proxy block silently fail or hit the wrong handler
  • root vs alias — see above; try_files checks paths relative to the same directive; a mismatch makes every fallback miss
# SPA — serve index.html for client routes try_files $uri $uri/ /index.html; # Named location fallback try_files $uri @app;

Debug: nginx -T for the effective location, confirm root, index, and that the fallback path or named location exists. Test with curl -v http://127.0.0.1/path — unexpected 404 often means the last try_files argument is wrong.

proxy_pass URI rewriting surprises

A trailing slash on proxy_pass strips the matched location prefix:

# location /api/ + proxy_pass http://backend/ # /api/users → http://backend/users # location /api/ + proxy_pass http://backend/api/ # /api/users → http://backend/api/users

When proxy_pass has no trailing slash and the location has no trailing slash either, the full URI — including the location prefix — is passed through unchanged:

# location /api + proxy_pass http://backend # /api/users → http://backend/api/users

rewrite adds another layer: a rule before proxy_pass changes what gets forwarded regardless of the trailing-slash rule. Trace with error_log debug or test with curl -v and compare the upstream request path to what the backend expects.

TLS / certificate issues

ssl_certificate should contain the full chain (leaf certificate + intermediates) in modern Nginx. Omitting intermediates is a very common cause of TLS errors — nginx -t passes, but browsers and mobile clients fail with incomplete chain warnings.

# Full chain — certificate + intermediates concatenated ssl_certificate /etc/ssl/certs/example_fullchain.crt; ssl_certificate_key /etc/ssl/private/example.key; openssl s_client -connect example.com:443 -servername example.com # Check certificate expiry openssl x509 -in cert.pem -noout -dates

Verify the chain with openssl s_client — look for "Verify return code: 0 (ok)". Let's Encrypt and similar CAs often provide fullchain.pem; use that for ssl_certificate, not cert.pem alone.

Debugging workflow

1. Config test and dump

nginx -t nginx -T | less

2. Error log while reproducing

tail -f /var/log/nginx/error.log curl -v http://127.0.0.1/problem-url , journalctl -u nginx -f

3. Test upstream independently

curl -v http://127.0.0.1:8000/health ss -tlnp | grep 8000

Practice scenarios

Hands-on Nginx scenarios on live Linux VMs: nginx

Cheatsheet →
SadServersSadServers

Real-world Linux and DevOps scenarios for hands-on learning and technical assessment.

Uptime Robot ratio (30 days)
Product
  • Scenarios
  • For Individuals
  • For Businesses
  • Pricing
Resources
  • FAQ
  • Blog
  • Newsletter
Company
  • About Us
  • Support
  • Privacy Policy
  • Terms of Service
  • Contact
Connect With Us
info@sadservers.com

Made in Canada 🇨🇦
Updated: 2026-06-13 16:06 UTC – 2d2950a