Web Server Errors (502, 503, 403, 404)¶
The HTTP status code is your fastest diagnostic clue: each one points at a different layer of the stack. A 5xx almost always means the backend (PHP-FPM or an app server) is unhappy, while a 4xx usually means the web server's own config or filesystem is wrong. This playbook maps each code to its cause and the command that confirms it.
Tested on
AlmaLinux 9 / RHEL 9 (and Rocky/CentOS Stream 9) with Nginx 1.20+, Apache (httpd) 2.4, and PHP-FPM 8.x, SELinux enforcing. Paths differ on Debian/Ubuntu: logs live under /var/log/nginx/ and /var/log/apache2/, the PHP-FPM service is named php8.x-fpm, and Apache is apache2 — but the diagnostic logic is identical.
Symptom¶
A browser or curl returns one of these, often as a bare error page from the web server rather than the application:
- 502 Bad Gateway — Nginx accepted the request but the upstream gave it nothing usable.
- 503 Service Unavailable — the server is up but temporarily can't handle the request.
- 504 Gateway Timeout — the upstream is alive but too slow to answer.
- 403 Forbidden — the server found the resource but refuses to serve it.
- 404 Not Found — the server looked where it was told and found nothing.
Likely causes¶
| Code | Usual cause |
|---|---|
| 502 Bad Gateway | Upstream (PHP-FPM / app server) is down, crashed, or Nginx fastcgi_pass/proxy_pass points at the wrong socket or port |
| 503 Service Unavailable | Backend overloaded, app in maintenance mode, or PHP-FPM hit its pm.max_children ceiling |
| 504 Gateway Timeout | Upstream too slow; request exceeds fastcgi_read_timeout / proxy_read_timeout |
| 403 Forbidden | File/dir permissions, missing index file with autoindex off, or an SELinux-mislabeled docroot (not httpd_sys_content_t) |
| 404 Not Found | Wrong root/DocumentRoot, or a try_files / location mismatch |
On RHEL, 403 and 502 are frequently SELinux
A docroot copied into place keeps the wrong SELinux label and the worker (httpd_t) can't read it → 403. A reverse proxy or PHP socket the worker can't reach because httpd_can_network_connect is off → 502/connection refused. Neither shows up in ls -l. Always check the audit log. See SELinux.
Diagnose¶
The logs tell you almost everything. Read them first, before changing config.
Read the logs¶
sudo tail -n 50 /var/log/nginx/error.log # Nginx upstream + permission errors
sudo journalctl -u php-fpm -n 50 # PHP-FPM crashes / max_children warnings
sudo tail -n 50 /var/log/php-fpm/www-error.log # PHP fatal errors
sudo tail -n 50 /var/log/httpd/error_log # Apache (if using Apache)
Typical smoking guns:
connect() failed (111: Connection refused) while connecting to upstream→ upstream down/wrong address (502).server reached pm.max_children setting→ PHP-FPM saturated (503).upstream timed out (110: ...)→ slow backend (504).Permission deniedor an AVC referencing your docroot → permissions or SELinux (403).
Validate the config¶
sudo nginx -t # Nginx syntax + which configs are loaded
sudo apachectl configtest # Apache equivalent (a.k.a. httpd -t)
Test locally, bypassing the network and DNS¶
curl -I http://127.0.0.1/ # see the status code straight from the server
curl -I http://127.0.0.1/somepage.php # exercise the PHP path
Confirm the upstream is actually listening¶
For a 502, verify PHP-FPM (or the app server) is up and that Nginx is pointed at the same socket/port:
systemctl status php-fpm
ss -tlnp | grep -E 'php-fpm|:9000' # TCP upstream listening?
ls -l /run/php-fpm/www.sock # Unix-socket upstream exists?
grep -R fastcgi_pass /etc/nginx/ # what Nginx is dialing
grep -E 'listen' /etc/php-fpm.d/www.conf # what PHP-FPM bound to
The fastcgi_pass in Nginx and the listen in the PHP-FPM pool must match exactly.
Check SELinux¶
getenforce
sudo ausearch -m avc -ts recent # AVC denials touching httpd_t / the docroot
ls -Z /var/www/html # docroot should be httpd_sys_content_t
Check docroot ownership and labels (for 403/404)¶
ls -l /var/www/html # readable by the worker user?
ls -Z /var/www/html # correct SELinux type?
namei -l /var/www/html/index.php # traverse bits on every parent dir
Fix¶
Match the fix to the code and to what the logs told you.
502 Bad Gateway — restore/repoint the upstream¶
sudo systemctl restart php-fpm # bring a crashed pool back
sudo systemctl enable --now php-fpm # ensure it starts on boot
If the socket/port is wrong, align Nginx with PHP-FPM and reload:
# /etc/nginx/conf.d/site.conf — must match php-fpm's listen=
# fastcgi_pass unix:/run/php-fpm/www.sock;
sudo nginx -t && sudo systemctl reload nginx
If the AVC shows httpd_t blocked from connecting to the upstream (common with TCP sockets or reverse proxies):
503 Service Unavailable — relieve the backend¶
Raise PHP-FPM's worker ceiling in the pool config, then restart:
# /etc/php-fpm.d/www.conf
# pm.max_children = 50 # raise based on RAM / avg process size
# pm.max_requests = 500
sudo systemctl restart php-fpm
If it's an intentional maintenance page, clear it once the deploy finishes. See PHP & PHP-FPM for sizing pm.* correctly.
504 Gateway Timeout — give the slow upstream more time (or make it faster)¶
# in the relevant location {} block
fastcgi_read_timeout 120s; # for PHP-FPM
proxy_read_timeout 120s; # for proxy_pass backends
Raising the timeout is a band-aid; also investigate the slow query/endpoint behind it.
403 Forbidden — permissions, index, or label¶
sudo chown -R root:nginx /var/www/html # worker group can read
sudo chmod -R o+rX /var/www/html # readable + traversable
sudo restorecon -Rv /var/www/html # fix SELinux labels (the usual RHEL fix)
If there's genuinely no index file and you intend a directory listing, enable autoindex on; (Nginx) or Options +Indexes (Apache) — otherwise add the missing index.html/index.php. See Permission Denied Errors for the full permission/label walkthrough.
404 Not Found — fix the path mapping¶
Confirm root/DocumentRoot points where the files actually live, and that try_files/location match the request:
root /var/www/html;
index index.php index.html;
location / { try_files $uri $uri/ /index.php?$query_string; }
For Apache, check DocumentRoot and the matching <Directory> block, then sudo apachectl configtest && sudo systemctl reload httpd. See Nginx and Apache.
Prevent¶
- Monitor the upstream. Alert on PHP-FPM being down and on
pm.max_children reachedin the logs, so 502/503 are caught before users report them. Watch service health via Logs & journald. - Always config-test before reload. Wire
nginx -t/apachectl configtestinto your deploy so a typo never takes the site down. - Stay SELinux-aware. Run
restorecon -Rvon the docroot after every deploy and set the righthttpd_*booleans up front — don't disable SELinux to make a 403/502 go away. See SELinux. - Size PHP-FPM to the box. Set
pm.max_childrenfrom available RAM and average process size, not a guess, to avoid chronic 503s under load. - Set realistic timeouts. Tune
fastcgi_read_timeout/proxy_read_timeoutto your real workload and fix slow endpoints rather than only raising the limit.