The right file owners and permissions for a PHP site
Uploads fail, WordPress asks for FTP details, Laravel shows a 500 error, or someone tells you to "just chmod 777". Most of these come down to one question: which user does PHP run as, and what may that user change? This guide answers it with commands you can copy.
1. Three users matter
- The PHP user. PHP-FPM runs your code as the
userset in its pool. Whatever PHP must change (uploads, cache, logs) has to be writable by this user. - The web server user. Nginx runs as
www-data. It only needs to read images, CSS and JavaScript, and enter the folders they're in. - You, or the user you deploy with. You change the code.
See which user each PHP pool runs as:
grep -E '^(user|group) ' /etc/php/8.3/fpm/pool.d/*.conf
/etc/php/8.3/fpm/pool.d/shop.conf:user = shop
/etc/php/8.3/fpm/pool.d/shop.conf:group = shop
/etc/php/8.3/fpm/pool.d/www.conf:user = www-data
/etc/php/8.3/fpm/pool.d/www.conf:group = www-data
Use your PHP version in the path (8.4 on Debian 13). Then check which pool your site uses: the fastcgi_pass line in its Nginx file names the pool's socket. Out of the box there is one pool, www, running as www-data for every site.
2. The simple setup we recommend: one user per site
Give each site its own user and its own PHP pool, and let that user own all of the site's files. Our guide to one PHP-FPM pool per site sets this up. For a site user called shop:
sudo chown -R shop:shop /var/www/shop.example/public
sudo find /var/www/shop.example/public -type d -exec chmod 755 {} +
sudo find /var/www/shop.example/public -type f -exec chmod 644 {} +
- 755 on folders: the owner may change them, everyone else may only open them.
- 644 on files: the owner may change them, everyone else may only read them.
That's all. PHP runs as shop, so uploads, WordPress updates and Laravel's storage folder just work, with no special folders. A break-in through this site can change this site, but not the others. Nginx can read the static files because of the last digit (the "others" part).
The pool guide also closes the site folder itself (chmod 750 /var/www/shop.example) and adds www-data to the shop group, so other site users can't look inside. In our test, the user of a second site got Permission denied. Keep both parts: when we took www-data out of the group, Nginx returned 404 (not 403) for the CSS file, with this in its error log:
stat() "/var/www/shop.example/public/index.php" failed (13: Permission denied)
Group changes only apply to Nginx after a restart: sudo systemctl restart nginx.
Files with passwords
Make the file with the database password readable by the site user only:
sudo chmod 600 /var/www/shop.example/public/wp-config.php
For Laravel, the same for .env. PHP still reads it, because PHP runs as the owner. Check that the web server can't:
sudo -u www-data cat /var/www/shop.example/public/wp-config.php
cat: /var/www/shop.example/public/wp-config.php: Permission denied
Run commands as the site user
Run wp, composer and php artisan as the site user, not as root, so new files get the right owner:
sudo -u shop php artisan config:cache
If you copy files in as root (with sudo unzip, sudo git pull or scp as root), run the chown line again afterwards. This finds files with another owner:
sudo find /var/www/shop.example/public ! -user shop
In our test, with the files owned by root, PHP could no longer write uploads. WordPress then asks for FTP details when you update. See harden WordPress for more on that.
3. The other setup: a deploy user and www-data
Some people keep one shared pool running as www-data, and a separate deploy user owns the code. Then PHP can't change its own code, which is good. But you must open the folders PHP writes to, one by one:
sudo chown -R deploy:www-data /var/www/app
sudo find /var/www/app -type d -exec chmod 755 {} +
sudo find /var/www/app -type f -exec chmod 644 {} +
Then make only the writable folders writable for the www-data group:
- WordPress:
wp-content/uploads(andwp-contentitself if you want WordPress to install updates and plugins; most people who choose this setup update withwp-cliinstead). - Laravel:
storageandbootstrap/cache.
cd /var/www/app
sudo chmod -R ug+rwX storage bootstrap/cache
sudo find storage bootstrap/cache -type d -exec chmod g+s {} +
ug+rwX lets the owner and the group read and write (the capital X adds "open" only to folders). g+s on a folder makes new files in it belong to the www-data group too. In our test, the Laravel app gave a 500 error before these lines and worked after them, while PHP still couldn't change the code.
The catch: files PHP creates belong to www-data and are only writable by it. In our test, after the site had written its log and cache, php artisan cache:clear as deploy failed with "Failed to clear cache", and logging failed with:
The stream or file "storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied
Running the same command as the PHP user worked: sudo -u www-data php artisan cache:clear. So in this setup, run Artisan and wp-cli as www-data. That's one reason we recommend the setup in step 2: there is only one user to think about.
One more downside: with a shared www-data pool, every site on the server runs as the same user, so one hacked site can change the uploads of all the others.
4. Why never 777
777 means "everyone on this server may change this". It hides the real problem (the wrong owner) and opens a new one. In our test, we set an uploads folder to 777, and the user of another site wrote a PHP file into it with no trouble. A hacked plugin on any site, or any user on the server, can do the same, and Nginx will run that file. With 2775 and the right group, the same attempt got Permission denied.
Find anything that everyone can write to:
sudo find /var/www -perm -o+w -not -type l
It should print nothing. If it prints files or folders, take away the "everyone may write" part, then fix the owner as in step 2 or 3:
sudo find /var/www -perm -o+w -not -type l -exec chmod o-w {} +
Also stop PHP from running inside the uploads folder. Our WordPress hardening guide shows the Nginx lines.
5. Still getting errors?
- 403, 404 or "Permission denied" in the Nginx log: a folder on the way is closed. Run
namei -lon the file. See fix 403 Forbidden and File not found. - "Access denied." on a PHP page: the PHP user can't read the file. Check its owner and permissions.
- Uploads fail in WordPress: the PHP user can't write to
wp-content/uploads. Compare the folder's owner with the pool'suser. - Laravel 500 error: often
storageisn't writable. See fix a Laravel 500 error.
Quick list
- Check the PHP user:
grep -E '^(user|group) ' /etc/php/*/fpm/pool.d/*.conf. - Best: one user per site owns everything; 755 folders, 644 files.
- Password files:
wp-config.phpand.envat 600. - Deploy user + www-data: group
www-data, writable foldersug+rwXandg+s; run Artisan and wp-cli aswww-data. - Never 777.
find /var/www -perm -o+wshould print nothing.
Official sources
- PHP manual: PHP-FPM pool settings (
user,group,listen.owner) - WordPress: changing file permissions
- Laravel: directory permissions
- GNU coreutils: file permissions (what
X,sand the numbers mean)
Next steps: give each site its own PHP-FPM pool, or let one person upload files to one site with SFTP.
ServerNest is being built to set up one user per site and warn you about files anyone can write to. Join early access to try it first.