NGINX / PHP / Server Setup

NGINX and PHP-FPM. What my permissions should be?

by , , revisited on

We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Being an avid StackExchange user, I could see how many users completely lack an understanding of the proper permissions model in the most popular, LEMP stack.

You can see recommendations that 777 is never good, but I could not see a simple guide on permissions that can be used as a reference point for everyone.

The following permissions/ownership model applies to all NGINX/PHP-FPM websites and allows you to host websites without any problems, in a secure way.

PHP-FPM user (as known as the website user)

The PHP-FPM user should be a special user that you create for running your website, whether it is Magento, WordPress, or anything.
Its characteristics are the following:

  • This is the user that PHP-FPM will execute scripts with
  • This user must be unique. Do not reuse an existing user account. Create a separate user for each website!
  • Do not reuse any sudo capable users. If your website user is ubuntu or centos, or, root – you’re asking for much trouble.
  • Do not use www-data or nginx as website user. This is wrong and will lead to more trouble!
  • The username should reflect either the domain name of the website that it “runs”, or the type of corresponding CMS, e.g. magento for a Magento website; or example for example.com website.

Let’s create this user:

useradd example

Now, set its password by running:

passwd example

Each website in PHP-FPM should be run under a separate pool. In the pool settings file, e.g. /etc/php-fpm.d/example.com.conf, you must set things to match with the created username:

listen = /var/run/php-fpm/example.com.sock
listen.owner = example
listen.group = example
listen.mode = 0660
user = example
group = example

The webserver user

NGINX must run with it own unprivileged user, which is nginx (RHEL-based systems) or www-data (Debian-based systems).

This is the “global” webserver user that is used for all websites. So the configuration is straightforward and translates to the following directives in /etc/nginx/nginx.conf:

user nginx;

Connecting website and webserver users

Now the magic. We must connect things up so that NGINX (webserver) user can read files that belong to the website user’s group.

This will allow us to control what NGINX can read or not, via group chmod permission bit.

usermod -a -G example nginx

This reads as: add nginx user to group example.

File ownership (chown)

Here is a simple rule: all the files should be owned by the website user and the website user’s group:

chown -R example:example /path/to/website/files

Incorrect chown examples:

  • www-data:www-data
  • example:nginx
  • nginx:nginx

Correct chown example:

  • example:example
  • foo:foo

Permissions (chmod)

The following general chmod setup will allow for any website to function properly:

chmod -R u=rwX,g=rX,o= /path/to/website/files

This translates to the following:

  • Website user (example) can read, write all files, and read all directories
  • Website group (webserver user) can read all files and traverse all directories, but not write
  • All other users cannot read or write anything

In octal notation, this results in 0750 chmod for all directories and 0640 for all files.

If you simply stick to this permissions model, you will not encounter any chmod / chown issues in the future.
This is the only proper model, functionality, and security-wise.

These general permissions are quite secure already out of the box. But of course, if you know your website structure, you may want to tighten up permissions even further.

For example, see Magento 1.x lockdown chmod.

A read-only user

When you want to provide a read-only access for someone else, you can leverage setfacl program. For example, to allow user foo to read a directory and all files inside:

setfacl --recursive --modify u:foo:rX,d:u:foo:rX /path/to/website/files

Troubleshooting tips

To see all directory and file permissions, up to a specific path, use this command:

namei -om /path/to/file/or/directory
  1. Shnoulle

    With chmod -R u=rwX,g=rX,o= /path/to/website/files ,

    If you have assets in /path/to/website/files/assets/css for example : nginx can not read/load it …

    How do you manage this?
    A potential solution ACL for nginx/www-data user.

    • Danila Vershinin

      NGINX will be able to read the files.

      css directory will be owned by example:example (owned by example user and by example group). Because we added nginx user (or www-data, depends on distro) as a member of the site user group, and the directory chmod allows for group reads, it will be able to access the directory just fine.

      See the “Connecting website and webserver users” section. This is the essential step: the webserver user is a member of each website’s user group. Not the other way around.

      All the files are owned by example:example. Where necessary for NGINX to read the files, the group permission bit is used to control what it can and what it can’t read.

      • Steve M


        Thank you for your article. I followed your instructions and configured nginx and php-fpm to work the same way, but I have issues with stylesheets. I have WordPress installation and stylesheets and other statics aren’t loaded properly. In browser’s console it says “Resource interpreted as Stylesheet but transferred with MIME type text/html”. I figured it’s statics going through php-fpm and not straight through nginx. I still haven’t got a clue how to fix it though.

        • Danila Vershinin

          Your error rather indicates a case when the browser receives a 404 (not found) or similar error, generated by NGINX, when accessing /some/style.css or /some/script.js. Those have Content-Type: text/html, which is of course neither a stylesheet nor a script.

          It doesn’t matter here WordPress or not you’re dealing with. The above permission setup applies to any NGINX/PHP-FPM website without any flaws or problems.

          Your issue means that NGINX cannot read the files. You haven’t followed some of the steps as outlined above. Make sure nginx user in website user’s group and the permissions were set as chmod -R u=rwX,g=rX,o= /path/to/website/files

  2. suges

    It really helped a lot, thank you, Danila.

  3. Person

    What about using Linux ACLs? No need to modify the owner of the files, but still allow access to any folder that needs it.
    This is an example for Laravel: https://stackoverflow.com/a/41268166/12869323

    • Danila Vershinin

      Bear with me 🙂 You really meant “no need to set the user ownership and permissions right”.

      In the case of NGINX (which can be applied to Apache as well, depending on a mode it runs with) and PHP-FPM, there are basically 2 services interacting with each other: NGINX is one user, PHP-FPM is another.

      Two services mean two different Linux users. They can be connected through the group permission bit.
      “Connected” means both of the mentioned services (users) can access/modify the files without giving problems to each other.

      Funnily enough, as far as your referenced StackOverflow question, the accepted/most-upvoted answer almost got it right.
      But never far enough right. At least for any of the stacks of “Apache + PHP-FPM” or “NGINX + PHP-FPM,” it won’t be right:

      In the section “Webserver as owner (the way most people do it, and the Laravel doc’s way)”, even its title is wrong.
      I don’t think there is such a reference in Laravel doc that would dare to state that all files should be owned by www-data,
      which is outright wrong.
      Instead, the right thing: every website has its own dedicated username (which PHP-FPM pool runs with),
      and all the particular website files are owned by their respective website user.
      The correct part in that section was about connecting users through adding www-data to the user’s group.
      As far as chmod, any of the 755 or 644 permissions will allow any Linux user on the server to read anyone’s website files.
      Those should be 750 and 640 max, unless you really want to grant access to all Linux users on the system
      (which in the context of website files, I think is a case of “never”).

      In the section “Your user as owner” now it’s incorrect the other way around.
      The files are now owned correctly by the dedicated “website user”, but he does have to identify
      which directories have to be written to by the Apache user www-data. Likely so because the question itself is about
      Apache running with mod_php, which is now more or less deprecated way to run PHP in the first place.

      As for the referenced answer. Only when you have 3 users, or when you’re using Apache+mod_php – only then setfacl may be applicable. Otherwise using it is only a patch on top of an often incorrect simple permission model of chown/chmod.

      If you’re running PHP as an Apache module, then there are 2 users, but there is a more severe disconnection between who often manages/uploads the files (SFTP user, human being), and who reads/executes PHP scripts among those files (www-data, Apache user).
      The default Linux mask usually results in the new directories/files created with group chmod that allows for reading only.
      But since in this setup sometimes directories are being created by the webserver user (as a result of running scripts/web installs, etc.),
      those created directories only end up being readable to the human being who cannot change/delete such directories.

      Then (again, for Apache+mod_php) the solution can easily be to set umask 002 for the Apache, e.g.:

      echo "umask 002" >> /etc/sysconfig/httpd

      And then all the files/directories created by it will be readily writable to the www-data group. And to finalize this approach for this kind of setup, you’d do the magic part as described above, just in the other way around: add the SFTP user to the www-data group. That’s it, no setfacls.

      The setfactl is great when there are really three users. Having three distinct users working on the same set of files pretty much introduces requirement to use it because chmod has only 3 bits and the last bit is for “all others”.

      I say, why would you want to use setfacl if you haven’t managed to reap the value of even the first 2 bits of the basic Linux permission system.

  4. Amit Saini

    Hello Danila Vershinin,
    Thanks for shared this useful information. I have installed magento2 on private server wth ubuntu and nginx. I have created a new user and make changes as per you guide. But I want to know by which user I need to run the command process? Because magento root files owner is new user that I have created, but this user has not root user permissions. So, for commands another user called “ubuntu” is used or not?

    • Danila Vershinin

      It’s pretty straightforward: for interacting with your website, like running SSH command magento or working with files over SFTP – use your website user.

      Use your ubuntu user (or any other user with sudo privileges) only for administrating your server. That is, for example, installing or upgrading server packages. Do not use your ubuntu user for interacting with your website files.

  5. CM

    GREAT explanation!!!
    It helped me a lot.

    I believe there’s one additional setting required:

    The same value assigned to ‘listen’ in the pool settings file must be assigned to ‘fastcgi_pass’ in the Nginx Server Block.

    PHP-FPM pool config:
    listen = /var/run/php-fpm/example.com.sock

    Nginx Server Block
    fastcgi_pass unix:/var/run/php-fpm/example.com.sock

    (using unix socket in this example)

  6. iddqd_x

    Hi Danila! Your post is the clearest I’ve seen on the net. Everything works fine, but I don’t understand how to safely give additional user account access to the website files. All files owned by example:example, I’ve added user foobar and added him to example group (nginx user also in the group example). So in this case I should use chmod 760 right? How safe this configuration? Or it can be achieved in another way? And probably I should also set SGID (sticky bit) attribute? Thank you.

    • Danila Vershinin

      I’d say when you really need to give access to another account (besides the site user and nginx), then use setfacl (extended ACL for Linux).
      This will let you tune access beyond the bits provided by chmod.

      However, if it’s being done for the sake of “adding developers” for a website, or other project management, this is still bad.
      For this, a git repository is most appropriate. Developers should not have access to the live system.

      • iddqd_x

        I’m really appreciate for your so fast answer. The situation is pretty standard. I have one user “example” (it owns all files and fpm runs by this account). And every developer use this single account to access ssh. Each developer has its own git account to push/pull changes. But this situation is not secure by design when developers user same ssh account.

        So my goal is to restrict access to “example” account and provide unique ssh account for each developer and keep the owner of the files by “example” user/group. What is your opinion about “-o” flag for “useradd” command which creates user account with non-unique UID? I suppose that would do the trick but how safe it is. In this case I don’t need to set higher chmod as developers accounts will have same UID/GID as “example” user.

        • Danila Vershinin

          Likewise, my answer is the same. Don’t let developers SSH at all. There should not be “their account”, neither they should access the live account. If you have the time to puzzle your head with how to give them access, you likely have also the time to set up website updating through git hooks. A good developer SSH account on your server is none 🙂 The only thing they would have access to is git and nothing else. Development by a sane developer happens in their local machine. Not on a server of any kind (not even on dev server – that is for integration/testing).

  7. Dibs

    So, I did this, though I am not certain I FULLY understand it…

    All seems to be running fine but nginx -t throws some errors now.

    These are permission issues on /var/log/nginx/error.log and my letsencrypt certs – I don’t really understand this as user:nginx is still running the web server?

    • Danila Vershinin

      What are the exact word to word errors you receive?

      • Dibs

        My error was being a moron and running nginx -t without proper privledges

  8. Phil T.

    This means “webserver user” has no write access to ‘wp-content/*’, right? So, is sftp or ssh required for plug-in management etc in WordPress?

    • Danila Vershinin

      No, it means webserver user (nginx) can read to the wp-content, but can’t write; and website user (example) can do both.
      Both SSH and WP-admin management of plugins will be functional.

  9. Jake

    How would you apply this method to Apache?

  10. vseager

    I have followed your guide and it’s working for my Magento 2 installation, however when I run command line commands I run into problems where they seem to be running as www-data. For example, when I run bin/magento setup:di:compile I get the following error: “The requested class did not generate properly, because the ‘generated’ directory permission is read-only.”.

    Before running the command I have tried resetting ownership recursively on the whole Magento directory to example:example, and I’m running the command when logged in as example, but the bin/magento command always seems to be running as www-data.

    • Danila Vershinin

      It only means that either your server is misconfigured and still runs as www-data and/or that you’re not running chown command as root. It should be run with root, simply because you can’t have one user take over ownership of other users files. Root user can 😏

      • vseager

        Thanks Danila, I’ll double check server configuration. I’m running chown with sudo

  11. zaries

    Thanx your a life saver!

  12. Rich

    I’m still getting 403 Forbidden errors using your instructions. New user for a site. I’m using passenger with nginx but that shouldn’t change anything, correct? What should I try? I’m trying to run these in my /home/myuser/sites directory, so maybe its parents are standing in the way?

    • Danila Vershinin

      Home directories typically have permissions of 0700, so that would surely be standing in a way unless you put it 0750 at least. For that reason, I am not recommending the use of home dirs. The canonical location, in my opinion, for web files, is /srv/www/example.com/public (you need to create all the directories along the way, and ensure they are each at least 0750).

      • Rich

        I’m on AlmaLinux 8.6 which currently shows the following (default path to nginx sites):

        [2022_Aug_16 11:04:39 user3@server_f ~] namei -om /usr/share/nginx/html/vhost1
        f: /usr/share/nginx/html/vhost1
        dr-xr-xr-x root root /
        drwxr-xr-x root root usr
        drwxr-xr-x root root share
        drwxr-xr-x root root nginx
        drwxr-xr-x root root html
        drwxr-xr-x root root vhost1

        Not sure how that’s going to show up here in the comments. nginx -t shows all good. But still getting 403:
        2022/08/16 11:07:33 [error] 73646#0: *1730 directory index of "/usr/share/nginx/html/vhost1/" is forbidden, client:, server: vhost1.com, request: "GET / HTTP/1.1", host: "www.vhost1.com"
        Not sure why it’s asking for a directory index.

        • Danila Vershinin

          Provided that you’ve already set up everything as in the article, the vhost1 directory should be owned by your PHP-FPM user and group, so you will chown -R example:example vhost1. NGINX is basically telling you it can’t look inside /usr/share/nginx/html/vhost1 for finding an index file like index.html, etc. In other words, it can’t browse/can’t see that directory’s contents.

  13. Andreas

    Wow, this is really cool, makes total sense and works like a charm. Thanx

  14. jackso

    Nice writeup and glad I found this guide. Unfortunately I ran into issue with bootstrap folder and storage folders with nginx

    drwxrwx--- 3 test1 test1 4096 Nov 30 12:44 bootstrap
    drwxrwx--- 7 test1 test1 4096 Nov 30 12:46 storage

    Here is my setup:

    /var/www/app1 <-- user = test1
    /var/www/app2 <-- user = test2

    If I use chmod -R u=rwX,g=rwX,o= instead of the one listed here then it works with bootstrap and storage folder. Maybe I missed something? I went through the steps few times already.

    • Danila Vershinin

      It’s not clear what “works” in your case and what’s not. What’s the actual issue with those folders? Have you set up PHP-FPM pools to run as those users?

      • jackso

        Will try to explain. Hope it makes sense.

        Using chmod -R u=rwX,g=rX,o= as you suggest will give me this drwxr-x--- resulting in folders (storage and bootstrap) unwritable error. Laravel log showing: can’t write to path /var/www/app1/storage/…

        php8.1-fpm test1.conf
        user = test1
        group = test1
        listen = /run/php/php8.1-fpm.sock
        listen.owner = test1
        listen.group = test1
        listen.mode = 0660

        I’m unable to convert to your method so have to go back to how most docs suggesting the following:

        sudo chgrp -R www-data storage bootstrap/cache
        sudo chmod -R ug+rwx storage bootstrap/cache
        result: drwxrwx---

        Anything else you suggest I check?

        • Danila Vershinin

          The leading drwx in your chmod means that the user owner of the file/directory can do anything with it (read and write). So there shouldn’t be any problem.
          Unless: 1) you haven’t configured the correct PHP-FPM pool definition with listen.owner=test1 2) you haven’t chowned the files/directories to the test1 user. All should be test1.

  15. sanjeev

    If i dont want to create PHP-fpm pool and i dont want to create new user and groups.. I want to use default pool so do i need to set file permission to www-data:www-data replacing example:example.

    chown -R example:example /path/to/website/files

    • Danila Vershinin

      If you don’t want to create a separate PHP-FPM pool/users, then you’re doing it wrong, then you can continue doing everything the wrong way, no more no less 🙂

  16. Tobias

    Why the “usermod -a -G example www-data” way? The follow is easier i think:

    listen = /var/run/php-fpm/example.com.sock
    listen.owner = www-data
    listen.group = www-data
    listen.mode = 0660
    user = example
    group = www-data

    File permissions
    chown -R example:www-data /path/to/website/files
    chmod 750 to all directories
    chmod 640 to all files

    Website user have full read/write access and nginx (www-data) just read access. Your opinion?

    • Danila Vershinin

      Because then any user from any website can at minimum read each other websites’ files. Furthermore, it’s just illogical: you run PHP-FPM with webserver’s user. These are 2 separate daemons. Would you swap names to another person? Same thing 🙂

  17. Andreas

    Thank you for this great tutorial. I use this approach since I came across it on your website.
    But I am still struggling to integrate my deployment flow. When I have to update, I change the user to my github user, then cd into the directory, make a git pull, make a npm run build and then switch user and group again. And during that time, the website is down. Even though this process does not take long, it is a bit frustrating.
    Do you have any suggestion for a deployment without downtime?


Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.