Jonathan Bird Web Development

How to Fix "Failed to Open Stream: Permission Denied" Error in Laravel 13 (2026 Guide)

Last updated: April 1, 2026

If you've ever deployed a Laravel application (or even just pulled a fresh project from Git) and been greeted by a "failed to open stream: Permission denied" error, you're definitely not alone. This is one of the most common issues that Laravel developers run into, and it affects everyone from beginners setting up their first project to experienced developers configuring production servers.

In this guide, I'll walk you through what causes the permission denied error, how to diagnose it, and the various ways to fix it in Laravel 13. Whether you're dealing with log files, view compilation, session storage, or deployment issues, I've got you covered in this article.

What is the "Failed to Open Stream: Permission Denied" Error?

This error occurs when PHP tries to write to a file or directory and the operating system says no. Laravel needs to write to several directories during normal operation, and if the web server process doesn't have the right permissions, things break.

The error shows up in a few different forms depending on what Laravel is trying to do:

Log file errors (the most common):

1UnexpectedValueException: The stream or file "/var/www/html/storage/logs/laravel.log" could not be opened in append mode: failed to open stream: Permission denied

View compilation errors:

1ErrorException: file_put_contents(/var/www/html/storage/framework/views/abc123.php): failed to open stream: Permission denied

Session write errors:

1ErrorException: file_put_contents(/var/www/html/storage/framework/sessions/abc123): failed to open stream: Permission denied

Bootstrap cache errors:

1ErrorException: file_put_contents(/var/www/html/bootstrap/cache/packages.php): failed to open stream: Permission denied

The path in the error message is the key to figuring out what's gone wrong. In almost every case, it points to somewhere inside storage/ or bootstrap/cache/.

Common Causes of the Permission Denied Error

Before jumping into fixes, it helps to understand why this error occurs in the first place.

Wrong File Ownership

This is the root cause the vast majority of the time. Two different users typically interact with your Laravel project:

  1. Your deploy or SSH user (e.g., forge, ubuntu, deployer) who runs git pull, composer install, and php artisan commands
  2. The web server user (e.g., www-data for Nginx/Apache on Ubuntu, apache on CentOS, nginx on some systems)

When your deploy user creates or modifies files, the web server user can't write to them unless group permissions are set up correctly. And vice versa.

Incorrect Directory Permissions

Even with the right ownership, directories might have permissions that are too restrictive. A directory with 755 permissions only allows the owner to write. If the web server is running as a different user, it can't write to those directories.

Running Artisan Commands as Root

Running sudo php artisan or having cron jobs execute as root creates files owned by root:root. Your web server (running as www-data) then can't write to those files. This is especially problematic with:

  • Log files: the daily log channel creates a new file each day, and whichever process writes first owns it
  • Cache files from php artisan config:cache, route:cache, or view:cache
  • The packages.php and services.php files in bootstrap/cache/ created by composer install
  • Queue workers running under a different user

Missing Directories After a Fresh Clone

Laravel's .gitignore excludes the contents of storage/framework/ subdirectories. After a fresh git clone, these directories might be missing entirely:

  • storage/framework/cache/data/
  • storage/framework/sessions/
  • storage/framework/views/
  • storage/logs/

Without these directories, you'll either get "Permission denied" or a related InvalidArgumentException: Please provide a valid cache path error.

SELinux Blocking Writes

On CentOS, RHEL, and Fedora systems with SELinux in enforcing mode, standard Unix permissions aren't enough. SELinux uses its own access control system that operates independently, and the httpd process is confined to specific security contexts. Your permissions might look correct, but SELinux is silently blocking writes.

Docker Volume Permission Mismatches

Docker containers run PHP-FPM as a specific UID/GID (typically 33 on Debian-based images, 82 on Alpine). When you mount your project directory as a volume, the host UID and container UID often don't match, which means files that are writable on your host machine aren't writable inside the container.

How to Fix the Permission Denied Error

Let's work through the solutions from most common to more advanced scenarios.

Fix 1: Set Correct Ownership and Permissions

This fixes the problem for most people. Run these commands from your project root on your server:

1# Set ownership: your deploy user owns files, web server group can write
2sudo chown -R $USER:www-data storage bootstrap/cache
3 
4# Set directory permissions to 775 (owner + group can read/write/traverse)
5sudo find storage bootstrap/cache -type d -exec chmod 775 {} \;
6 
7# Set file permissions to 664 (owner + group can read/write)
8sudo find storage bootstrap/cache -type f -exec chmod 664 {} \;

Replace www-data with your web server's group name. You can find this by checking your web server configuration:

1# For Nginx
2ps aux | grep nginx
3 
4# For Apache
5ps aux | grep apache
6 
7# Or check the config directly
8grep -r "user" /etc/nginx/nginx.conf
9grep -r "User" /etc/apache2/apache2.conf

Important: The rest of your project files (app/, config/, routes/, etc.) should stay at 755 for directories and 644 for files. Only storage/ and bootstrap/cache/ need the more permissive settings.

Fix 2: Use the Setgid Bit

The problem with Fix 1 is that new files created inside these directories might not inherit the correct group. The setgid bit solves this by making new files and directories automatically inherit the parent directory's group:

1sudo chown -R $USER:www-data storage bootstrap/cache
2 
3# Set directories to 2775 (setgid + owner/group read/write/traverse)
4sudo find storage bootstrap/cache -type d -exec chmod 2775 {} \;
5 
6# Set files to 664 (owner/group read/write, no execute)
7sudo find storage bootstrap/cache -type f -exec chmod 664 {} \;

The 2 at the start of 2775 is the setgid bit. With this set on directories, any new files or directories created inside storage/ or bootstrap/cache/ will automatically belong to the www-data group, regardless of which user created them.

This is the approach I'd recommend for most production servers because it handles the ongoing ownership problem, not just the current state.

Fix 3: Create Missing Directories

If you've just cloned the project and directories are missing, create them:

1mkdir -p storage/framework/{cache/data,sessions,views}
2mkdir -p storage/logs
3mkdir -p storage/app/public
4mkdir -p bootstrap/cache

Then set the permissions as described in Fix 1 or Fix 2.

You can also run Laravel's storage link command while you're at it:

1php artisan storage:link

To prevent this from happening again, make sure your project has .gitignore files in these directories so Git tracks the empty folders. Laravel ships with these by default (e.g., storage/framework/cache/.gitignore), but they can get accidentally removed.

Fix 4: Use ACLs for Multi-User Environments

If you have multiple users that need write access (for example, a deploy user, a queue worker user, and the web server user), POSIX ACLs give you more fine-grained control than standard Unix permissions:

1# Install ACL utilities if needed
2sudo apt-get install acl
3 
4# Set default ACL so www-data always gets read/write on new files
5sudo setfacl -R -m default:u:www-data:rwX storage/
6sudo setfacl -R -m default:u:www-data:rwX bootstrap/cache/
7 
8# Also set permissions on existing files
9sudo setfacl -R -m u:www-data:rwX storage/
10sudo setfacl -R -m u:www-data:rwX bootstrap/cache/

The capital X (instead of lowercase x) applies execute permission only to directories and files that already have execute permission, which is usually what you want.

For environments with a deploy user and a web server user:

1# Set default ACLs for new files
2sudo setfacl -Rdm u:www-data:rwx,u:deploy:rwx storage/
3sudo setfacl -Rdm u:www-data:rwx,u:deploy:rwx bootstrap/cache/
4 
5# Set ACLs on existing files
6sudo setfacl -Rm u:www-data:rwX,u:deploy:rwX storage/
7sudo setfacl -Rm u:www-data:rwX,u:deploy:rwX bootstrap/cache/

Fix 5: Configure Log File Permissions

If the error is specifically about log files, you can tell Laravel what permissions to use when creating new log files. In config/logging.php:

1'channels' => [
2 'single' => [
3 'driver' => 'single',
4 'path' => storage_path('logs/laravel.log'),
5 'permission' => 0664,
6 'level' => env('LOG_LEVEL', 'debug'),
7 ],
8 'daily' => [
9 'driver' => 'daily',
10 'path' => storage_path('logs/laravel.log'),
11 'days' => 14,
12 'permission' => 0664,
13 'level' => env('LOG_LEVEL', 'debug'),
14 ],
15],

The permission key sets the file permissions when Laravel creates a new log file. Setting it to 0664 allows both the file owner and group to write.

Fix 6: Stop Running Artisan as Root

If you've been running commands with sudo php artisan, stop. Files created by root won't be writable by your web server. Instead:

1# WRONG: creates files owned by root
2sudo php artisan config:cache
3sudo php artisan view:cache
4 
5# CORRECT: creates files owned by your deploy user
6php artisan config:cache
7php artisan view:cache

If a command genuinely needs elevated permissions (like writing to system directories), run it as the web server user instead:

1sudo -u www-data php artisan config:cache

For cron jobs and scheduled tasks, make sure they run as the correct user:

1# WRONG
2* * * * * root cd /var/www/html && php artisan schedule:run
3
4# CORRECT
5* * * * * forge cd /var/www/html && php artisan schedule:run

If you're using Laravel Forge, the schedule is already configured to run as the forge user.

Fix 7: Fix SELinux Contexts (CentOS/RHEL/Fedora)

If you're on a system with SELinux and standard permission fixes aren't working, you need to set the correct security context:

1# Check if SELinux is enforcing
2getenforce

If it returns Enforcing, set the correct context for Laravel's writable directories:

1# Set the context (survives restorecon)
2sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/storage(/.*)?"
3sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html/bootstrap/cache(/.*)?"
4 
5# Apply the context
6sudo restorecon -R /var/www/html/storage
7sudo restorecon -R /var/www/html/bootstrap/cache

The httpd_sys_rw_content_t type tells SELinux that the httpd process is allowed to read and write to these paths.

Don't just disable SELinux (setenforce 0) as a fix. It's there for a reason, and properly configuring the contexts is straightforward once you know the commands.

Fix 8: Fix Docker Volume Permissions

For Docker environments where you don't need to mount volumes, the cleanest approach is to copy files with the correct ownership and run as www-data:

1FROM php:8.4-fpm-alpine
2 
3WORKDIR /var/www/html
4 
5COPY --chown=www-data:www-data . .
6 
7RUN chmod -R 775 storage bootstrap/cache
8 
9USER www-data

If you're using Docker Compose with mounted volumes, permissions need to be fixed at startup. This requires the container to start as root, so don't combine this with the USER www-data approach above. Instead, use an entrypoint script:

1#!/bin/sh
2chown -R www-data:www-data /var/www/html/storage
3chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
4exec php-fpm

PHP-FPM handles user switching internally. The master process runs as root and spawns worker processes as www-data (configured in the pool's www.conf), so you don't need su-exec or gosu here.

Another approach is to match the container's UID to your host UID at build time. This avoids the chown on every startup. For Debian-based images:

1FROM php:8.4-fpm
2 
3ARG USER_ID=1000
4ARG GROUP_ID=1000
5 
6RUN groupmod -g ${GROUP_ID} www-data && \
7 usermod -u ${USER_ID} -g ${GROUP_ID} www-data
8 
9USER www-data

For Alpine-based images, you'll need to install the shadow package first since Alpine doesn't include groupmod/usermod by default:

1FROM php:8.4-fpm-alpine
2 
3ARG USER_ID=1000
4ARG GROUP_ID=1000
5 
6RUN apk add --no-cache shadow && \
7 groupmod -g ${GROUP_ID} www-data && \
8 usermod -u ${USER_ID} -g ${GROUP_ID} www-data
9 
10USER www-data

Debugging Permission Issues

If the fix isn't immediately obvious, here are some debugging strategies.

Check Current Permissions and Ownership

Start by looking at what's actually set:

1ls -la storage/
2ls -la storage/logs/
3ls -la storage/framework/
4ls -la bootstrap/cache/

The output shows the owner, group, and permissions for each file and directory. Look for files owned by root or a user that doesn't match your web server.

Find Which User the Web Server Runs As

1# Check the running process
2ps aux | grep -E "(nginx|apache|php-fpm)" | grep -v grep

The first column shows the user. On most Ubuntu systems, it's www-data.

Check for SELinux Issues

1# Check if SELinux is the problem
2getenforce
3 
4# Check the current context on storage
5ls -Z storage/
6 
7# Look for SELinux denials in the audit log
8sudo ausearch -m avc -ts recent

If ausearch shows denials for httpd_t trying to write to httpd_sys_content_t, that's your problem. The files have the wrong SELinux context.

Verify Permissions from PHP's Perspective

Use Artisan tinker to check what PHP sees:

1php artisan tinker
1is_writable(storage_path());
2// true = PHP can write, false = permission denied
3 
4is_writable(storage_path('logs'));
5 
6is_writable(base_path('bootstrap/cache'));
7 
8// Check which user PHP is running as
9echo exec('whoami');

Check for Open Basedir Restrictions

If you're on shared hosting, PHP's open_basedir might be restricting access:

1php -i | grep open_basedir

If this is set, PHP can only write to directories within the specified paths.

Special Cases

Laravel Forge

Forge runs deployments as the forge user and configures Nginx to use www-data. The default deploy script handles permissions, but if you're having issues, add this to the end of your deploy script in the Forge dashboard:

1sudo chown -R forge:www-data storage bootstrap/cache
2sudo chmod -R 2775 storage bootstrap/cache

Shared Hosting (cPanel)

Shared hosting with suPHP or CGI mode runs PHP as the file owner, so you typically only need:

1chmod -R 755 storage bootstrap/cache

Note the 755 instead of 775, because there's no group user to worry about since PHP runs as you. Also, never use 777 on shared hosting. Many hosts will actually reject it and show a 500 error because it's a security risk on a shared server.

If php artisan storage:link fails because your host has disabled symlink(), create the link manually via SSH:

1ln -s /home/youruser/project/storage/app/public /home/youruser/project/public/storage

Local Development (macOS with Herd or Valet)

On macOS, permissions are rarely an issue because tools like Herd and Valet run PHP as your local user. If you do hit this error locally (perhaps after pulling changes that included files with different ownership), a quick fix is:

1sudo chown -R $USER:staff storage bootstrap/cache
2chmod -R 775 storage bootstrap/cache

Queue Workers

Queue workers can be a sneaky source of permission issues. If a queued job writes to the log or creates files, those files will be owned by whichever user the queue worker runs as. Make sure your queue worker runs as the same user (or at least the same group) as your web server:

1# If using Supervisor
2[program:laravel-worker]
3command=php /var/www/html/artisan queue:work
4user=forge

Summary

The "failed to open stream: Permission denied" error in Laravel is almost always a file ownership or permissions issue. Here's a quick checklist:

  1. Set correct ownership with chown -R $USER:www-data storage bootstrap/cache
  2. Set correct permissions with chmod -R 2775 storage bootstrap/cache (the setgid bit helps with ongoing file creation)
  3. Create missing directories after a fresh clone with mkdir -p storage/framework/{cache/data,sessions,views}
  4. Stop running artisan as root to avoid creating root-owned files
  5. Configure log permissions in config/logging.php if log files are the specific problem
  6. Fix SELinux contexts on CentOS/RHEL with semanage fcontext
  7. Match Docker UIDs if using containerised deployments

The most common scenario is simply that the web server user can't write to storage/ because ownership or group permissions aren't set up. Fix 1 or Fix 2 will solve it for the majority of cases.


Having trouble with permissions or other Laravel issues? I specialise in Laravel development and debugging complex application issues. Get in touch to discuss your project.

Topics

Syntax highlighting by Torchlight

More articles

How to Fix "Target Class Does Not Exist" Error in Laravel 13 (2026 Guide)

The "Target class does not exist" error in Laravel is usually caused by incorrect controller references, missing imports, or autoloading issues. This guide covers all the common causes and how to fix them in Laravel 13.

Read article

Laravel's Failover Queue Driver: How to Never Lose a Job

Laravel's failover queue driver quietly switches to a backup connection when your primary queue goes down. Here's how to set it up, monitor it, and make sure your jobs always get processed.

Read article

Talk to me about your website project