Laravel error
Laravel: The stream or file could not be opened
The error
The stream or file "/var/www/app/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission deniedMonolog could not write laravel.log. The permission is the visible problem; the reason two different users own files in that directory is the one worth fixing.
The stream or file "/var/www/app/storage/logs/laravel.log" could not be
opened in append mode: Failed to open stream: Permission denied
Monolog cannot open the log file. The application usually dies with a 500 at this point, because the failure happens while it is trying to report something else - and so the original error is never recorded.
That is the detail worth noticing. This exception is almost always standing in front of a different one.
Two users, one directory
The permission bit is a symptom. The cause is that more than one identity writes into storage/logs:
- the web server, as
www-dataornginx - the deploy, as whatever your CI uses
- cron and queue workers, frequently as
rootbecause that was easiest
Whichever writes first owns the file. The rest get this.
sudo chown -R deploy:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
sudo find storage -type d -exec chmod g+s {} \;The last line is the one people skip and the one that stops it coming back: the setgid bit makes new files inherit the directory's group, so a file created by cron is still writable by the web server.
chmod 777 also makes the error stop. It makes every process on the machine able to write into a directory your application reads from, which is a bad trade for five seconds of convenience.
The midnight version
With the daily driver the filename carries the date - laravel-2026-10-28.log - so a new file is created every midnight by whatever logs first.
If a nightly cron running as root gets there before the first web request, the web server cannot append to today's file, and the site starts throwing at 00:00 with nothing deployed. Teams chase this for a long time because the timing suggests a scheduled task is broken, and the scheduled task is fine - it just logged first.
Running cron and queue workers as the same user as the web server removes the whole class of problem. If they must differ, the setgid bit above is what makes it survive.
Stop the logger from hiding the real error
While you are here, make sure a logging failure cannot swallow the thing it was logging. In config/logging.php:
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'stderr'],
'ignore_exceptions' => false,
],A second channel that does not touch the filesystem means a permission problem costs you one destination rather than the whole record.
In a container, go further and drop the file entirely - stderr only, collected by the platform. There is no ownership question if nothing is written to disk, and a container filesystem is the wrong place for anything you want to keep.
What it is usually covering for
Once logging works, look at what was being logged. This error arriving in a burst almost always means the application started throwing something else at the same time, and the permissions were a pre-existing condition that only became visible when the logger was finally asked to do some work.
Infrastructure faults that present as application errors are a large share of what support work actually involves, and this is the most common one.
