🔥

Laravel + Docker環境の「解決不能に見えた」500エラー:原因特定までの全記録

に公開

はじめに

Laravel, React, Docker(Vite)という構成は、モダンなWebアプリケーション開発における一般的な選択肢の一つです。しかし、その柔軟性と強力さの裏で、一度問題が発生すると原因の特定が困難になるケースも少なくありません。

本記事では、開発初期段階のログインAPI実装時に発生した、単純に見えて極めて根深い500 Internal Server Errorとの格闘の記録を共有します。当初は単純なデータベース設定の問題かと思われましたが、最終的にはDockerの環境構築そのものに潜む、複数の問題が連鎖して引き起こされていたことが判明しました。

同様の環境で開発を行うエンジニアが、不可解なエラーに直面した際のデバッグの一助となれば幸いです。


初期診断:一般的な原因と最初のアプローチ

最初に500 Internal Server Errorに遭遇した際、私たちはまず基本的な原因を疑いました。以下は、その時点での問題認識と対処法です。

主要な問題

  • ログインAPI (POST /api/v1/auth/login) で500 Internal Server Errorが発生。

初期仮説

  1. データベースマイグレーションが未実行: usersテーブルが存在しないため、認証処理で失敗している。
  2. PHPのDBドライバー不足: Dockerコンテナ内のPHPに、MySQLへ接続するためのドライバーがインストールされていない。

初期対応

これらの仮説に基づき、以下の手順を実行しました。

  1. Dockerコンテナの正常起動を確認
    docker-compose ps
    
  2. データベースマイグレーションの実行
    docker-compose exec backend php artisan migrate
    
  3. テストデータのシーディング
    docker-compose exec backend php artisan db:seed
    

多くの開発環境では、この手順で問題が解決します。しかし、今回は全てのコマンドが成功したにもかかわらず、500エラーは解消されませんでした。これが、長期にわたる詳細な調査の始まりでした。


問題の深層:複数要因の連鎖

エラーが解消されないことから、原因はさらに深い階層にあると判断し、バックエンドのログ (storage/logs/laravel.log) の詳細な分析を開始しました。その結果、エラーメッセージは状況に応じて変化し、複数の問題が複雑に絡み合っていることが明らかになりました。

1. 環境変数と設定キャッシュの罠

最初にログで確認されたのは、LaravelのMailManagerDatabaseConnectorに起因する設定不足エラーでした。

  • The "url" option is required.
  • MAIL_FROM_ADDRESS=null is not supported.
  • getaddrinfo for mysql failed: Name or service not known

これらのエラーは、都度.envファイルを修正することで対処しました。

  • APP_URL=http://localhost:8000
  • MAIL_FROM_ADDRESS=hello@example.com
  • DB_HOST=db

しかし、.envを修正しphp artisan config:clearを実行しても、修正前の設定値(例:DB_HOST=mysql)でエラーが発生し続けるという不可解な現象に遭遇しました。php artisan tinkerconfig('database.connections.mysql.host')を実行すると正しい値(db)が返されるにもかかわらず、Webリクエスト経由では古い値が参照され続けていたのです。

これは、Laravelのファイルベースのキャッシュ(bootstrap/cache/config.php)が、config:clearコマンドで正常に削除されずに残存していたことが原因でした。最終的に、コンテナに直接入りrm bootstrap/cache/config.phpを実行することで、このキャッシュ問題を強制的に解決しました。

2. 複数のdocker-compose.ymlによる環境の歪み

上記のキャッシュ問題を解決してもなお、根本的なDB接続エラーは解消されませんでした。
最終的な原因は、プロジェクトの構造にありました。

/ (project-root)
├── docker-compose.yml  # (A) サービス名: backend, db
├── api/
│   ├── docker-compose.yml  # (B) サービス名: laravel.test, mysql
│   └── ... (Laravel Project)
└── ... (Frontend Project)

プロジェクト内に、内容の異なるdocker-compose.ymlが2つ存在していたのです。開発者は(A)のファイルで環境を操作しているつもりでしたが、過去の経緯から(B)のファイルが参照される瞬間があったのかもしれません。この**「信頼できる唯一の情報源(Single Source of Truth)」の欠如**が、DB_HOSTdbにすべきかmysqlにすべきかという根本的な混乱を生み、デバッグを著しく困難にしました。


最終的な解決策と技術的教訓

全ての調査を経て、以下の手順で問題は完全に解決しました。

  1. 単一のdocker-compose.ymlを正とする: プロジェクトルートのdocker-compose.yml(A)を唯一の定義ファイルとし、apiディレクトリ内の(B)は削除。
  2. .envの値を確定: (A)の定義(サービス名:db)に基づき、api/.envDB_HOSTdbに確定。
  3. 環境の完全再構築: 潜在的なキャッシュやボリュームの問題を根絶するため、以下のコマンドでDocker環境をゼロから再構築。
    # コンテナとボリュームを完全に削除
    docker-compose down -v
    
    # キャッシュを使わずにイメージを再ビルド
    docker-compose build --no-cache
    
    # コンテナを起動
    docker-compose up -d
    
  4. マイグレーションとシードの再実行: クリーンな環境で、改めてDBの初期化を実行。

この一連のデバッグから得られた教訓は以下の通りです。

  1. 設定ファイルは聖域である: docker-compose.ymlのような環境定義ファイルは、プロジェクト内に一つだけ存在させるべきです。これにより、設定の不整合や意図しない挙動を防ぎます。
  2. Docker環境のキャッシュは徹底的に疑う: config:clearで解決しない問題は、より深いキャッシュ層(Dockerイメージ、ボリューム、PHP OPcache)に起因する可能性があります。環境の完全な再構築(down -v)は、問題切り分けの強力な手段です。
  3. 500エラーはバックエンドログが全て: ブラウザに表示される500エラーは、単なる結果通知です。原因を特定するには、必ずlaravel.logを確認し、具体的なエラーメッセージとスタックトレースを分析する必要があります。

まとめ

当初の「マイグレーション未実行」という単純な仮説から始まったこの問題は、最終的にDocker環境の構成とキャッシュ管理という、より本質的なテーマへと繋がりました。この記事が、同様の「不可解なエラー」に直面した開発者にとって、冷静な問題解決の一助となることを願っています。

Discussion