記事一覧に戻る
Web技術

LaravelのtempnamエラーとHTTP 500を、書き込み権限から直す

デプロイ後のtempnamエラーとHTTP 500。storageとbootstrap/cacheの所有グループ、PHP実行ユーザー、setgidとumaskの関係から原因と復旧手順を整理する。

ノートと鉛筆のイラスト

デプロイ後のLaravelアプリケーションがHTTP 500を返し、ログに次のエラーが残ることがある。

tempnam(): file created in the system's temporary directory

この記事で扱うのは、Bladeテンプレートの保存先にPHPの実行ユーザーが書き込めず、この通知が例外になった事例だ。Laravelが生成するファイルの保存先と、Linuxの所有者・グループの関係を確かめると、原因を整理できる。

対象はLinux、PHP 8.4、Laravel 13系。フレームワークの実装は13.38.0で確認した。以下の配置先は説明用のパスで、www-dataも実行ユーザー・グループの一例として使う。

tempnamが、別の場所にファイルを作ろうとする理由

PHPのtempnam()は、指定したディレクトリに一意な名前の一時ファイルを作る関数だ。指定先が存在しない、または書き込めない場合、システムの一時ディレクトリへフォールバックすることがある。その際に通知を出すことは、PHP公式マニュアルにも記載されている。

Laravel 13.38.0のFilesystem::replace()は、保存するファイルと同じディレクトリに一時ファイルを作り、内容を書いてから目的の名前へ変更する。この一時ファイルの作成にtempnam()を使っている。

今回の事例では、次の順でHTTP 500に至っていた。

  1. リクエストを受け、Bladeテンプレートのコンパイルが必要になる。
  2. コンパイル結果の保存先に一時ファイルを作ろうとする。
  3. PHP実行ユーザーに書き込み権限がなく、tempnam()が別の場所へフォールバックする。
  4. その通知がErrorExceptionとして扱われ、リクエストが失敗する。

同じメッセージでも、指定先が存在しない場合はある。まずスタックトレースから、どの保存先への書き込みで失敗しているのかを確認する。

0775でも、PHPから書き込めるとは限らない

Laravelはstorageとbootstrap/cacheにファイルを書き込む。公式のデプロイガイドでも、これらにWeb側のプロセスから書き込める権限を求めている。標準的なBladeのコンパイル先はstorage/framework/viewsだが、設定を変更している場合は実際の保存先を確認する。

ディレクトリが0775でも、それだけでは十分ではない。通常のUnix権限では、それぞれ次の意味になる。

対象 権限 ディレクトリでできること
所有者 rwx 一覧取得・項目の作成や削除・通過
所有グループに属するユーザー rwx 一覧取得・項目の作成や削除・通過
その他 r-x 一覧取得・通過

たとえば、所有者と所有グループがどちらもデプロイ用アカウントで、PHPの実行ユーザーがそのグループに属していなければ、PHPには「その他」の権限が適用される。ディレクトリ内にファイルを作るための書き込み権限がない。

元になった事例も、この組み合わせだった。SSHで操作するユーザーからは書けても、Webリクエストを処理するユーザーからは書けなかった。

保存先と、実際のPHP実行ユーザーを確認する

アプリケーションのルートで、保存先と親ディレクトリの所有者・所有グループ・権限を確認する。

ls -ld storage storage/framework storage/framework/views bootstrap bootstrap/cache

続いて、PHPを実行しているユーザーとグループを調べる。PHP-FPM構成なら、プール設定のuserとgroupや、稼働中のワーカープロセスを確認する。Nginxの実行ユーザーとPHP-FPMの実行ユーザーが同じとは限らない。

実行ユーザーがwww-dataと分かった場合、アカウントの所属グループは次のように確認できる。

id www-data

SSH接続中に引数なしでidを実行しても、分かるのはSSH側のユーザー情報だ。PHPのプロセスに適用されている権限と区別して考える。グループ所属を変更した場合は、既存プロセスへの反映も確認する。

所有グループをそろえて、書き込み権限を付ける

ここでは、デプロイ用アカウントとPHP実行ユーザーが、書き込み用のグループを共有する構成を考える。例ではそのグループをwww-dataとする。

変更対象のファイルを所有し、変更先グループにも所属するアカウントであれば、次のように設定できる。条件を満たさない場合は管理権限が必要になる。パスとグループ名は実際の環境に合わせる。

cd /path/to/laravel-app

移動に成功し、artisan、storage、bootstrap/cacheがあることを確認してから、以下を実行する。

chgrp -R www-data storage bootstrap/cache
find storage bootstrap/cache -type d -exec chmod g+rwxs {} +
find storage bootstrap/cache -type f -exec chmod g+rw {} +

各コマンドの役割は次のとおりだ。

設定 目的
chgrp -R www-data 対象の所有グループを、PHP側から書き込めるグループにそろえる
ディレクトリのg+rwx グループに一覧取得・書き込み・通過の権限を与える
ディレクトリのg+s setgidを設定し、新しく作る項目が親ディレクトリのグループを継承するようにする
ファイルのg+rw グループから既存ファイルを読み書きできるようにする

setgidのグループ継承はLinuxのinodeマニュアルで説明されている。ディレクトリと通常ファイルを分けて設定することで、通常ファイルに実行権限を追加せずに済む。

この例はグループへの権限を追加するもので、既存の「その他」の権限は変更しない。すでに広すぎる権限がある環境では、別途その見直しも必要になる。アプリケーション全体を777にする必要はない。

権限を直したあとで、ビューを再生成する

書き込み可能になったら、コンパイル済みビューを削除する。

php artisan view:clear --no-interaction

view:clearはビューキャッシュを消すコマンドで、権限そのものは修正しない。Laravelは通常、必要になったビューをリクエスト時にコンパイルするため、削除後にWeb経由でページを開き、再生成できるかを確認する。ビューの最適化に関する公式資料にも、この動作が説明されている。

元になった復旧記録では、権限修正とビューのクリアに加え、アプリケーション独自のコンテンツ生成も実行されていた。その後、トップページ・一覧・検索・詳細ページがHTTP 200を返し、CSSとJavaScriptも取得できた。Webアクセスで再生成されたコンパイル済みビューの所有者・グループが、PHP側の実行ユーザー・グループになったことも確認されている。

独自の生成コマンドはアプリケーション固有の処理なので、読者の環境では必要なものだけを実行する。復旧確認では、HTTPステータスに加えて、期待した内容の表示、新しいエラーログの有無、再生成されたファイルの所有者・グループ・権限を確認する。

次のデプロイでも書き込める状態にする

一度権限を直しても、次のデプロイで保存先を作り直したり、別のユーザーで生成コマンドを実行したりすると、同じ問題が戻る可能性がある。保存先の所有グループと権限の設定を、デプロイ手順に組み込んでおく。

もう一つ注意したいのが、setgidと書き込み権限の違いだ。setgidがそろえるのは所有グループであり、新規ファイルのg+wまで保証するわけではない。新しく作る項目の権限には、作成プロセスのumaskや、アプリケーションが指定するモードも関係する。Linuxのumaskマニュアルでは、作成時の権限から指定のビットが取り除かれる仕組みが説明されている。

通常のファイルを0666で作る場合、umask 0022なら結果は0644になり、グループに書き込み権限は付かない。デプロイ処理やPHPの各プロセスが作るファイルを確認し、共有が必要な保存先に合わせてumaskや生成時の権限を設定する。

デプロイ用アカウントとPHP実行ユーザーの両方が必要なファイルを扱えること、そして次に作られるファイルも同じ条件を満たすこと。この二つを確かめると、復旧を次のデプロイにつなげられる。