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?
answer
- storage and bootstrap/cache must be writable
- first writer creates the file
- root-owned log blocks the web user
- run Artisan as the PHP user
- shared group with setgid
basics
~20 sAn 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 sApart 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
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.
Explain how the first writer owns a file, why daily logs make the error intermittent, and how group ownership with setgid prevents it.
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.
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