skip to content

After a root-run deploy, why can a Laravel site fail with 'laravel.log could not be opened in append mode', and how is it prevented?

level: middleimportance: should knowfreq 40%

answer

  1. storage and bootstrap/cache must be writable
  2. first writer creates the file
  3. root-owned log blocks the web user
  4. run Artisan as the PHP user
  5. shared group with setgid

basics

~20 s

An Artisan command run as root created storage/logs/laravel.log (or a cache file) owned by root, so the PHP-FPM user can no longer write to it. Run deploy commands as the web user and give storage and bootstrap/cache group-writable ownership.

solid answer

~50 s

Apart from a SQLite database file if you use one, Laravel writes at runtime to two trees: `storage` (logs, file sessions, file cache, compiled views, the maintenance file) and `bootstrap/cache`. Whichever process writes a file first owns it. If the deploy runs `php artisan migrate --force` or `optimize` as root and something logs, root creates `storage/logs/laravel.log`; the PHP-FPM user (for example `www-data`) then cannot append, and Monolog's `StreamHandler` throws `UnexpectedValueException` "could not be opened in append mode", which turns every logged event into an error. The fix: run deploy and cron Artisan commands as the same user as PHP-FPM (`sudo -u www-data php artisan ...`), and give `storage` and `bootstrap/cache` a shared group with group write and the setgid bit, so new files stay writable by the group. Correct the ownership of existing files once.

go deeper

for a junior

Recall that storage and bootstrap/cache must be writable by the PHP process, and that running Artisan as root can create files the web user cannot write.

for a middle

Explain how the first writer owns a file, why daily logs make the error intermittent, and how group ownership with setgid prevents it.

for a senior

Make the deploy and cron run as the PHP user, fix ownership once on shared storage, and never trade the fix for world-writable permissions.

for a principal

Standardise server users, groups and umask across hosts so permission drift cannot creep in between deploys.

## What Laravel writes at runtime The deployment docs say it directly: Laravel needs to write to `bootstrap/cache` and `storage`, and the web server's PHP process must be able to write there. Inside those directories: | Path | Written by | Contents | |---|---|---| | `storage/logs` | any process that logs | `laravel.log` or dated daily files | | `storage/framework/views` | Blade, or `view:cache` | compiled templates | | `storage/framework/cache` | the `file` cache store | cache entries | | `storage/framework/sessions` | the `file` session driver | session files | | `storage/framework/down` | `php artisan down` | the maintenance payload | | `bootstrap/cache` | `optimize`, `package:discover` | cached config, routes, events, packages, services | ## How the failure happens On a busy ticketing platform the deploy runs over SSH as `root`. During `php artisan migrate --force` a deprecation notice or an error is logged. Because `storage/logs/laravel.log` did not exist yet (a fresh server, a new daily file, or log rotation), root **creates** it with root ownership and a default mode that does not allow others to write. Minutes later a PHP-FPM worker running as `www-data` tries to log something. Monolog's `StreamHandler` cannot open the file for appending and throws an `UnexpectedValueException`: > The stream or file "/var/www/tickets/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied With the daily driver the problem can appear at midnight instead of at deploy time: whichever process writes first on a new day owns that day's file. Cron jobs that run `php artisan schedule:run` as root cause the same thing. The same pattern hits other paths. A compiled view created by root blocks Blade from recompiling it, and cache files created by root block the `file` cache store. ## Preventing it 1. **Run Artisan as the PHP user.** Deploy scripts and cron entries use `sudo -u www-data php artisan ...`, or the deploy connects as a user that shares the web group. 2. **Give the writable trees a shared group.** Set the group of `storage` and `bootstrap/cache` to the web server's group and make them group-writable. 3. **Set the setgid bit on directories.** New files and subdirectories then inherit the directory's group instead of the creator's primary group. 4. **Mind the creation mode.** Monolog-based file channels accept a `permission` option in the channel's config, which sets the mode of a newly created log file. 5. **Fix existing damage once.** Change ownership of files already created by root. ```bash chown -R deploy:www-data storage bootstrap/cache chmod -R ug+rwX storage bootstrap/cache find storage bootstrap/cache -type d -exec chmod g+s {} + ``` ## What not to do - **Make everything world-writable.** It hides the symptom and lets any local user alter caches that PHP later executes; `bootstrap/cache` files are PHP code that the app `require`s. - **Run PHP-FPM as root.** It removes the permission error and every safety boundary with it. - **Delete the log and move on.** The next root-run command recreates it. ## Diagnosing quickly 1. Read the path in the error; it names the file that cannot be opened. 2. `ls -l` on that file and its directory shows the owner, group and mode. 3. Compare with the user PHP-FPM runs as, and with the user your deploy and cron use. 4. Fix ownership, then fix the process that created the file. ## With release directories When `storage` is a shared directory symlinked into each release, the ownership fix is done once on `shared/storage`. Each release's `bootstrap/cache` is created during the deploy, so the deploy user must create it with the right group, which is another reason to run the deploy as a user in the web group rather than as root.

  • Why can a root-owned file in a Laravel app's bootstrap/cache be worse than a root-owned log file?
    Files in `bootstrap/cache` are PHP files the framework `require`s on every request. If the web user cannot rewrite them, later `package:discover` or cache rebuilds run as that user fail, leaving stale caches in place. Making them world-writable instead would let other local users change code PHP executes.
  • The Laravel log permission error only appears some mornings after midnight; what is going on?
    With the `daily` log channel, each day gets a new file, created by whichever process writes first that day. If a root cron job, such as a root-run `schedule:run`, logs first after midnight, it creates the file as root and the PHP-FPM user cannot append for the rest of the day.

saying these in an interview costs you the question

  • chmod -R 777 storage is the standard fix
  • Only storage/logs needs to be writable
  • Running deploy commands as root avoids permission problems
  • Laravel fixes file ownership automatically on the next request
  • bootstrap/cache is read-only and never written after install