Skip to content

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 denied or 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):

sudo setsebool -P httpd_can_network_connect on

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
sudo nginx -t && sudo systemctl reload nginx

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; }
sudo nginx -t && sudo systemctl reload nginx

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 reached in 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 configtest into your deploy so a typo never takes the site down.
  • Stay SELinux-aware. Run restorecon -Rv on the docroot after every deploy and set the right httpd_* 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_children from available RAM and average process size, not a guess, to avoid chronic 503s under load.
  • Set realistic timeouts. Tune fastcgi_read_timeout/proxy_read_timeout to your real workload and fix slow endpoints rather than only raising the limit.

Test yourself