The message failed to open stream: Permission denied is a PHP warning raised when a function such as fopen(), file_put_contents(), include, require, copy() or move_uploaded_file() asks the operating system for a file and the OS says no. PHP does not decide permissions; the kernel does, based on which user the PHP process runs as. The full line looks like:
Warning: file_put_contents(/var/www/example.com/storage/cache/a.json): failed to open stream: Permission denied in /var/www/example.com/app/Cache.php on line 41
Note the two paths. The first (inside the parentheses) is the file PHP could not open. The second is the PHP script that tried. All your work happens on the first path.
Step 1: find out who PHP runs as
PHP runs as the web server's or PHP-FPM pool's user, not as you. Check:
ps -eo user,comm | grep -E "php-fpm|apache2|httpd|nginx" | sort -u
grep -E "^(user|group)" /etc/php/*/fpm/pool.d/www.conf
Common users are www-data (Debian, Ubuntu), apache or nginx (RHEL family), and a per-site user on shared hosts and panels. From a PHP script, echo get_current_user(); echo exec('whoami'); shows the owner of the script and the running user (remove it right after).
Step 2: check every directory in the path
A file is reachable only if the user has execute (x) permission on every parent directory, plus the right permission on the file itself. namei shows each hop:
sudo -u www-data namei -l /var/www/example.com/storage/cache/a.json
ls -ld /var/www/example.com/storage/cache
A parent such as /var/www/example.com with mode 700 owned by another user blocks everything below it, even if the final folder is 777. To read a file, the user needs r on the file and x on the directories. To create or delete files, the user needs w and x on the containing directory.
Step 3: set ownership and modes correctly
Prefer fixing the owner over opening permissions wide:
sudo chown -R www-data:www-data /var/www/example.com/storage /var/www/example.com/cache
sudo find /var/www/example.com/storage -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/storage -type f -exec chmod 644 {} \;
If a deploy user owns the code and PHP only needs to write to a few folders, give only those folders to the PHP user, or use a shared group with the setgid bit (chmod g+ws) and add both users to it.
Step 4: SELinux or AppArmor
On RHEL, CentOS, Rocky and Alma, SELinux can deny access even when Unix permissions are right. Look for denials:
sudo ausearch -m avc -ts recent | tail
ls -Z /var/www/example.com/storage
Label writable directories so the web server may write to them:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/storage(/.*)?"
sudo restorecon -Rv /var/www/example.com/storage
On Ubuntu, check AppArmor logs in dmesg or /var/log/syslog if a profile restricts PHP.
Step 5: other causes
open_basedirproduces a different message ("open_basedir restriction in effect"), but check it anyway inphp.inior the pool config if only some paths fail.- Read-only file system or a full disk.
df -handtoucha test file. A mount may have flipped to read-only after errors. - Wrong path. A missing directory typically gives "No such file or directory," but a path that points to a directory instead of a file gives "failed to open stream" with different wording. Print the real path with
var_dump(realpath($path)). - Different user for CLI and web. A cron job run as root creates files that the web user cannot later overwrite. Run scheduled jobs as the same user as the web process.
- Docker volumes. The host UID owning a bind-mounted folder may not match the container's PHP user. Align UIDs or use
chownin the entrypoint. - Uploads:
move_uploaded_file()fails when the destination is not writable. Checkupload_tmp_diras well.
WordPress and frameworks
In WordPress the same error appears when plugins write to wp-content, or when updates fail; see could not create directory. In Laravel, storage and bootstrap/cache must be writable by the PHP user. In both cases the fix is the same: correct owner, correct mode, correct label.
Verify
- Re-run the failing action and confirm the warning is gone from the PHP error log (
/var/log/php-fpm/or the web server's error log). - Test as the PHP user:
sudo -u www-data touch /path/to/dir/testfile. - Check the page still returns the right status with the HTTP Status Checker. A fatal error after the warning often returns a 500; see fix 500 internal server error.
- Monitor the page with an Uptime Monitor and a keyword check.
Turn display_errors off in production and log errors to a file instead, since warnings print full server paths to visitors.
Next steps
- Compare with nginx permission denied (13).
- Read about headers errors in PHP headers already sent.
- See PHP allowed memory size exhausted.
- Harden the site with WordPress security hardening checklist.
FAQ
Should I set permissions to 777?
No. It makes files writable by every user on the system. Fix the owner instead and use 755 for directories and 644 for files, with write access only where PHP needs it.
Why does it work from the command line but not the browser?
Your shell user and the web server user are different accounts. Test with sudo -u www-data to reproduce what PHP sees.
The directory is writable but I still get the error. Why?
A parent directory may lack execute permission, SELinux may be blocking, or the filesystem may be read-only. Use namei -l and ausearch.
Is this warning a security problem?
The warning itself is not, but showing it to visitors leaks paths. Log errors privately.
Get notified when this fix updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.