500エラーの原因切り分け|Nginx・PHP-FPM・アプリを5分で特定する手順

ネットワーク診断

本番で500エラーが出た瞬間に、Nginx・PHP-FPM・アプリのどこで止まっているかを最短で見極められる状態を目指します。

本稿では「再現→ログ照合→モジュール別チェック→一次対処」を、実務で回せる順序に落とし込み、500エラー 切り分けを5分で完了させるための要点だけに絞って解説します。

詳細な流れを横断的に確認したい場合は Linuxトラブルシューティング総合ハブ から全体像をご覧いただけます。

この記事で解決すること(500を5分で所在特定する)

まず再現と範囲確認(URL限定か全体か/静的は出るか)

500エラーが発生したら、最初に「再現」と「範囲」を切り分けます。
特定のURLだけか、全体的に出ているのか、また静的ファイル(CSSや画像)が正常に配信されているかを調べることで、Nginx自体が落ちていないかを確認できます。

# 静的ファイル(例: CSS)が正常に取得できるか
curl -I http://localhost/css/app.css

# 500が出る特定URLを再現
curl -i http://localhost/specific-endpoint

静的ファイルは返るがアプリ経由のURLで500なら、PHP-FPMやアプリ層が疑わしいです。逆に静的すら返らないなら、Nginx設定や権限を確認する方向に絞れます。

環境軸の整理(本番のみ・特定ユーザーのみ・特定時間帯)

次に、障害が「どの環境」「どの条件」で発生するかを切り分けます。
本番だけか、特定ユーザーやIPだけか、ピーク時間帯だけなのかを押さえることで、原因のあたりを早めに付けられます。

# 本番環境だけ発生するか比較(本番・ステージング)
curl -i https://production.example.com/endpoint
curl -i https://staging.example.com/endpoint

# 特定IPのアクセスに絞ってログ確認
grep "203.0.113.45" /var/log/nginx/access.log | tail -n 20

# 発生時間帯の偏りを確認
grep "500" /var/log/nginx/access.log | awk '{print $4}' | cut -d: -f2 | sort | uniq -c

この整理を行えば、再現性があるのか、環境依存なのか、ユーザー条件なのかを素早く特定でき、次に進むべき確認ポイントを無駄なく選べます。

先にNginxで“手前止まり”を否定する(設定・アップストリーム・権限)

error.logとaccess.logの照合(同一request_id/timeで追跡)

Nginx内で即500になっていないかを、access.logの500行とerror.logの該当時刻(または$request_id)で突合します。$request_idをログに出していない環境でも、まずは直近の500を起点に誤配信や設定ミスを疑います。再現直後に以下で“1件の500”を特定し、同じ時刻のerror.logを引き当てます。

# 直近の500を1件抽出(common/combined 前提)
tail -n 500 /var/log/nginx/access.log | awk '$9=="500"{print; last=$0} END{print last}' | tail -n1

# 時刻キーで error.log を照合(例:02/Oct/2025:14:30)
ts="02/Oct/2025:14:30"
grep "$ts" /var/log/nginx/error.log | tail -n 50

$request_idを出している場合はID照合が一瞬で終わります。

# 直近500の request_id を推定($request_id が末尾フィールド例)
rid=$(tail -n 500 /var/log/nginx/access.log | awk '$9=="500"{rid=$NF} END{print rid}')
[ -n "$rid" ] && {
  echo "RID=$rid"
  grep -F "$rid" /var/log/nginx/access.log | tail -n 3
  grep -F "$rid" /var/log/nginx/error.log  | tail -n 50
}

upstreamの応答コードとconnect()/read()失敗の見分け

Nginx→上流(FastCGI/Proxy)の経路で接続不可か、接続はできたが応答異常かを切ります。connect() failedupstream timed out (110)は“到達できない/遅すぎる”兆候、upstream sent invalid headerはヘッダ整合やCGI応答の体裁不備が濃厚です。

# nginx 構文と有効設定の健全性
sudo nginx -t

# upstream 接続失敗やタイムアウトの典型メッセージを拾う
grep -E 'upstream .* (timed out|connect\(\) failed|Connection refused|invalid header)' /var/log/nginx/error.log | tail -n 20

# FastCGI の宛先(UNIXソケット or TCP)の生存確認
# 例: fastcgi_pass unix:/run/php-fpm/www.sock; または 127.0.0.1:9000
ss -ltnp | grep -E ':80|:443|:9000'
ls -l /run/php-fpm/www.sock 2>/dev/null || true

Nginxのproxy_passで別アプリ(:3000 等)を裏にしている場合は、裏のLISTENと応答も即チェックします。

# 例: app :3000 を裏に持つときの疎通(ヘルスエンドポイントを用意していればベター)
ss -ltnp | grep ':3000'
curl -sS -m 2 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/health || echo "app疎通NG"

権限・SELinux・所有者ミスでの13: Permission denied検知

静的配信やtry_filesの参照先で読み取り不可だと、Nginxは500/403を返します。13: Permission deniedがあれば、所有者/パーミッション/SELinuxコンテキストを即点検します。

# 配信対象の実体ファイル/ディレクトリを特定し、権限ツリーを辿る
path="/var/www/html/index.php"   # 例: 該当パスに置き換え
namei -l "$path"
ls -ld "$path" "$(dirname "$path")"

# Nginx実行ユーザでの読み取り可否(環境により 'nginx' や 'www-data')
sudo -u nginx test -r "$path" && echo "nginxユーザで読める" || echo "nginxユーザで読めない"

# SELinux 稼働とコンテキスト修復(稼働中のみ)
getenforce 2>/dev/null
ls -Z "$path" 2>/dev/null || true
sudo restorecon -Rv "$(dirname "$path")" 2>/dev/null || true

設定差し替えやデプロイ直後はinclude先の崩れやroot/aliasの整合ズレも頻出です。疑わしければ有効設定の全出力で目視します(秘匿値に留意)。

# 実際に読み込まれている全設定を俯瞰(出力は一時ファイルへ)
sudo nginx -T > /tmp/nginx.effective.conf 2>&1 && sed -n '1,120p' /tmp/nginx.effective.conf

以上で「Nginxの手前で止まっていないか」を短時間で否定できます。Nginx側が健全なら、次はPHP-FPMの枯渇・致命的エラーの線に進みます。

次にPHP-FPMの異常を確認(プール設定・ワーカー枯渇・致命的エラー)

php-fpm.logとslowlogでの兆候(pm.max_children reached 等)

まずは致命的エラーやワーカー枯渇が出ていないかをログで確認します。ディストリごとにパスやサービス名が違うため、共通手順で当てにいきます。pm.max_children reached はプール満杯、child exited with code 255 等は致命的エラーのサインです。

# サービス名の当て:systemd登録の fpm を探す
systemctl list-units | grep -E 'php.*fpm|php-fpm' || true

# 代表的ログの候補を直近から確認(存在するものだけヒット)
sudo sh -c 'for f in \
/var/log/php-fpm/error.log \
/var/log/php7*/fpm/error.log \
/var/log/php8*/fpm/error.log \
/var/log/php-fpm/www-error.log \
/var/log/php_errors.log; do
  [ -f "$f" ] && echo "==> $f" && tail -n 80 "$f"; done'

# journal 経由でサービスログ(サービス名は環境に置換:例 php8.2-fpm, php-fpm)
svc=$(systemctl list-units | awk '/php.*fpm|php-fpm/{print $1; exit}')
[ -n "$svc" ] && journalctl -u "$svc" -n 120 --no-pager | grep -E 'max_children|emergency|fatal|OOM|segfault|exited' -n || true

スロークエリの兆候はslowlogで把握します。設定が無効なら、まず有効化されているかを確認します(request_slowlog_timeout超過時にバックトレースを吐きます)。

# 有効なプール設定を特定(www.conf 等)
phpdir=$(php --ini 2>/dev/null | awk -F': ' '/Loaded Configuration File/{print $2}' | xargs dirname 2>/dev/null)
# よくある場所を総当たり
sudo sh -c 'grep -HnE "^\s*(pm\.|request_slowlog_timeout|slowlog)" \
/etc/php*/fpm/pool.d/*.conf 2>/dev/null || true'

# slowlogファイルがあれば直近を確認
sudo sh -c 'for f in /var/log/php*fpm/slowlog* /var/log/php*/fpm/*slow* 2>/dev/null; do
  [ -f "$f" ] && echo "==> $f" && tail -n 80 "$f";
done'

status/pingエンドポイントで健全性確認

/fpm-status/fpm-pingping.path)が有効なら、待機/実行/停止中プロセスやキュー状況を即座に把握できます。未開放なら次回メンテでの設置を検討します。

# ローカルから疎通(Nginxのlocationで連携している前提)
curl -sS -m 2 http://127.0.0.1/fpm-ping || echo "ping未設定の可能性"
curl -sS -m 2 http://127.0.0.1/fpm-status | sed -n '1,40p' || echo "status未設定の可能性"

# 出力例の読み方(目視でOK)
# pool: www
# accepted conn: 12345
# listen queue: 0          ← ここが増えるとリクエスト滞留
# max children reached: 7  ← 過去に枯渇が発生
# idle processes: 5 / active processes: 2 / total processes: 7

listen queue が増え続けるならプール容量不足の可能性が高いです。一次対処として一時的にpm.max_children引き上げアプリ側の重い処理の回避を検討します(恒久対応は計測前提)。

# 現在有効な pm.* 設定を抽出
sudo sh -c 'grep -HnE "^\s*pm\.(start_servers|max_children|max_spare_servers|min_spare_servers|max_requests)" /etc/php*/fpm/pool.d/*.conf 2>/dev/null'

# 有効設定の整合性チェックと再読込(ダウン無しで反映)
sudo php-fpm -t 2>/dev/null || sudo php8.2-fpm -t 2>/dev/null || true
sudo systemctl reload "$svc" 2>/dev/null || sudo service php*-fpm reload 2>/dev/null || true

display_errors=Off時の実エラー把握(error_log/php_admin_value)

本番は通常display_errors=Offのため、画面に出ない致命的エラーはログで捕捉します。php.iniとプールwww.confの**php_admin_value[error_log]catch_workers_output**の設定も確認します。

# どの ini が効いているか確認
php --ini

# エラーログの出力先と display_errors を確認
php -i | grep -E 'error_log|display_errors' -n

# プール別の error_log 指定(php_admin_value)を探索
sudo sh -c 'grep -HnE "php_(admin_)?value\[(error_log|display_errors)\]" /etc/php*/fpm/pool.d/*.conf 2>/dev/null || true'

# よくある致命的エラーを一気に拾う
sudo sh -c 'for f in \
/var/log/php-fpm/error.log \
/var/log/php*/fpm/error.log \
/var/log/php_errors.log; do
  [ -f "$f" ] && echo "==> $f" && grep -E "PHP Fatal error|Uncaught|Allowed memory size|Call to undefined function|Class .+ not found" "$f" | tail -n 50;
done'

メモリ不足Allowed memory size)はmemory_limit上げで一時回避できますが、根本はクエリ/ループ/巨大レスポンスの見直しです。

# 現行の memory_limit を確認
php -i | grep -E '^memory_limit' -n

# (一次回避案)pool の php_admin_value で memory_limit を局所的に上げ、reload
# ※恒久は計測・プロファイル必須。上げ過ぎはOOMを誘発
sudo sed -i 's/^\s*;*\s*php_admin_value\[memory_limit\].*/php_admin_value[memory_limit] = 512M/' /etc/php*/fpm/pool.d/www.conf 2>/dev/null || true
sudo systemctl reload "$svc" 2>/dev/null || true

要点
Nginx側が健全でも、max_children reachedlisten queue増加・致命的エラーの痕跡があればPHP-FPM/アプリ層のボトルネックです。ワーカー枯渇はプール拡張+重い処理の回避で一次収束、致命的エラーはログ位置を特定→該当リリース差分のロールバックが最短です。

最後にアプリ例外へ深掘り(フレームワークのログと設定)

例外トレースの場所(Laravel/Symfony/WordPress の定番パス)

Nginx・PHP-FPMが健全なら、アプリ例外のスタックトレースを直読します。まずはフレームワーク既定のログを最後尾から確認し、直近の例外でルート・コントローラ・クエリを掴みます。

# Laravel
sudo tail -n 120 storage/logs/laravel.log | sed -n '$-120,$p'
# 設定確認(ログチャネル/レベル)
grep -nE 'LOG_CHANNEL|LOG_LEVEL|APP_ENV|APP_DEBUG' .env 2>/dev/null || true
php artisan config:show | grep -nE 'log|logging' -n 2>/dev/null || true

# Symfony
sudo tail -n 120 var/log/prod.log
sudo tail -n 120 var/log/dev.log  # devに漏れていないか確認
# Monologハンドラの有無
grep -nR "monolog" -n config/ 2>/dev/null || true

# WordPress(WP_DEBUG_LOGが有効な場合)
grep -nE 'WP_DEBUG|WP_DEBUG_LOG|WP_DEBUG_DISPLAY' wp-config.php
sudo tail -n 120 /var/www/html/wp-content/debug.log 2>/dev/null || true

LaravelはAPP_DEBUG=falseでもstorage/logs/laravel.logに詳細を吐きます。同時刻のHTTPリクエストに紐づく[stacktrace]next exceptionを起点に、該当コミット差分(git show)を当てて一次回避(ロールバック/Feature Flag OFF)に備えます。

# 直近デプロイ差分で例外箇所に当たりを付ける
git log --since="2 hours ago" --oneline
git show HEAD --name-only | sed -n '1,120p'

環境変数・接続先(DB/Cache/外部API)失敗の一次代替とロールバック

500の多くは外部依存の失敗(DB/Redis/API)です。疎通と資格情報を即時に確かめ、落ちている場合は読み取り専用モードキャッシュ迂回で“落ちない状態”に寄せます。

# .envの接続先と資格情報を確認(秘匿に注意)
grep -nE 'DB_HOST|DB_(PORT|DATABASE|USERNAME)|REDIS_HOST|CACHE_DRIVER|QUEUE_CONNECTION|API_' .env

# DB疎通(MySQL/MariaDB or PostgreSQL)
mysql  -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" -e 'SELECT 1' "$DB_DATABASE" 2>&1 | tail -n 2
psql   "host=$PGHOST dbname=$PGDATABASE user=$PGUSER password=$PGPASSWORD" -c 'SELECT 1;' 2>&1 | tail -n 2

# Redis/キャッシュ
redis-cli -h "${REDIS_HOST:-127.0.0.1}" -p "${REDIS_PORT:-6379}" ping || echo "Redis疎通NG"
php artisan cache:clear   2>/dev/null || true
php artisan config:clear  2>/dev/null || true

# 外部API(タイムアウト短めで確認)
curl -sS -m 2 -o /dev/null -w '%{http_code}\n' "https://api.example.com/health" || echo "外部API疎通NG"

依存がNGなら、一次代替で落下を防ぎます(恒久は後追いでOK)。

# 一時的な読み取り専用モード(例:フラグenv)
# APP_READONLY=1 を読む分岐があるなら切替
sed -i 's/^APP_READONLY=.*/APP_READONLY=true/' .env && php artisan config:clear

# Laravelメンテナンス画面を素早く制御(短時間のロールバック時など)
php artisan down --render="errors::503" --retry=60
# ロールバック/設定修正後
php artisan up

WordPressなら、問題プラグイン停止テーマ切替で一次回避します(CLIが安全)。

# 問題プラグインを一括停止(WP-CLI)
wp plugin deactivate --all
# 直近更新のプラグインだけ停止(例)
wp plugin list --update=available --fields=name,status,update | awk '$3=="available"{print $1}' | xargs -r wp plugin deactivate
# 既定テーマへ切替
wp theme activate twentytwentyfive

再発防止の最小セット(ヘルスチェック・可観測性・閾値アラート)

一次収束後は同じ種類の500エラーを防ぐために、最小限の観測とガードを入れます。まずは“見える化”と“限界手前の通知”が要点です。

# 1) FPMステータス/アプリの簡易ヘルスを常設
#   /fpm-status, /fpm-ping は前節参照。アプリ側は /health に依存疎通も含める
#   例: DB/Redis/APIの簡易チェックを /health に集約(200/503を返す)

# 2) ログ量と500件数の定点監視(暫定: ローカルでの簡易可視化)
# 直近10分の500カウント
ts=$(date -d '10 minutes ago' '+%d/%b/%Y:%H:%M')
grep "$ts" /var/log/nginx/access.log | awk '$9=="500"{c++} END{print "last10m_500=", (c+0)}'

# 3) 閾値アラートの原始実装(cronで1分ごとに)
# last1mで500>30ならexit 2 など → 監視に拾わせる

加えて、以下を“最低限”として固めると500エラー 切り分けが次回から一段と速くなります。

  • リリース単位のFeature Flag:外部依存・重処理を即OFFにできる
  • 構成の一元ログ:アプリ例外(JSON化)+Nginxアクセス/エラーの相互参照
  • タイムアウト/リトライ方針:外部APIは“短タイムアウト+1回リトライ+フォールバック”
  • FPM容量ガードlisten queue増加でアラート、max_children reached記録を監視に送出

この段で「例外の具体箇所」「壊れている接続先」「一次代替の適用状況」まで判明すれば、ロールバック or ホットフィックスの判断に即移れます。

まとめ

サーバー障害の初動チェックを併用すれば、500エラーの切り分けも安定して再現できます。

本記事の手順は「Nginx→PHP-FPM→アプリ」の順に500エラー 切り分けを最短化する設計です。まずはログ照合で所在を特定し、FPMの枯渇や致命的エラーを押さえ、最後にアプリ例外と外部依存の一次代替で落ちない状態を作る——ここまでを5〜10分で回すのが実務最適です。

運用フローに組み込むなら、直後の健全性確認としてデプロイ後のヘルスチェックを定着させ、容量起点の障害はログ肥大の安全な削減と合わせて未然防止すると効果が高まります。
本記事のチェックリストは自社スタック向けに調整可能です。運用標準化やテンプレ整備のご相談があれば、そのまま雛形化して横展開できます。

参考リンク

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする