Nginx and PHP-FPM, bash script for creating new vhost’s under separate fpm pools

Using worker pools in PHP-FPM can allow you to easily separate out and isolate virtual hosts that make use of PHP. PHP-FPM allows you to run multiple pools of processes all spawned from the master one and each pool can run as a different user and/or group. Each pool can be further isolated by running in a chroot environment and by overriding the default php.ini values on a per pool basis.

Running PHP for each vhost under a different user/group can help to stop a vulnerability in one site potentially exposing another vhost, it can also stop one malicious owner of a vhost from been able to use PHP to access the files of another site owned by someone else on the same server (in a shared hosting environment).

The process of setting up the web server config and a new PHP-FPM pool for each new vhost on a server can become a rather time consuming and boring process. However as this process follows a fairly standard set of steps it can be easily scripted.

The following bash script was designed for use on Debian and Ubuntu, however it only needs a few modifications to use it on CentOS (the lines should be commented in the script).

What does the script do:

  • Creates a new system user for the site
  • Creates a new vhost config file for nginx using a basic template
  • Creates a new PHP-FPM pool with the uid and gid set to that of the new system user
  • Creates a new directory for the site, within the new users home directory
  • Reloads Nginx to allow the new vhost to be detected
  • Restarts PHP-FPM to generate the new pool of PHP workers

How it works:


Under your Nginx config directory (probably /etc/nginx) you need to create two new directories (if you don’t already have them):
  • sites-available
  • sites-enabled

In the “sites-available” dir goes the the individual config files for each vhost, each new vhost using a new file. Under the “sites-enabled” directory goes a symlink to the config file in the sites available directory, this gives you the ability to disable sites without removing the contents of the site or its config. You will also need to add the following (within the http section) to main Nginx config file (nginx.conf – lives under /etc/nginx/nginx.conf on debian) to get Nginx to automatically look in the sites-enabled directory.

http {
  # Load all vhosts !
  include /etc/nginx/sites-enabled/*.conf;
Replacing the path of the your nginx configuration if needed.
The web root of the new site will be /home/<new user>/public_html if you’re setting up hosting for a PHP site that holds most of its code outside of the web root (most MVC frameworks do this such as Symfony and FuelPHP) then you can specify the web root directory interactively when the script runs.


By default the script will put the config file for new pools under /etc/php5/fpm/pool.d, however you can easily change this at the top of the script to where ever you want. You will need to add the following line to your php-fpm.conf file (lives under: /etc/php5/fpm/php-fpm.conf typically on Debian):


This will tell PHP-FPM where to look for other config files, which in this case will be the individual pool config files (one per pool).

How to use it:

Simply download the tar file at the end of the this article and extract it. If your not sure how to extract a tar file either read the man pages(man tar) or use the command below:

tar -xzf create_php_vhost.tar.gz

Once you have extracted the archive you will need to change the permissions on the file to make it executable (if it isn’t already):

chmod u+x

Then run the script passing to it the domain name as the only parameter.


The script will then take you through the steps required to setup the Nginx configuration, the new system user and the PHP-FPM pool. Once all that is complete it will restart Nginx and PHP-FPM ready for your new vhost.
An example screen session is shown below:

Creating hosting for:
Please specify the username for this site?
Adding user `example' ...
Adding new group `example' (1000) ...
Adding new user `example' (1000) with group `example' ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for example
Enter the new value, or press ENTER for the default
        Full Name []: Example User
        Room Number []:
        Work Phone []:
        Home Phone []:
        Other []:
Is the information correct? [Y/n] y
Would you like to change to web root directory (y/n)?
Enter the new web root dir (after the public_html/)
How many FPM servers would you like by default:
Min number of FPM servers would you like:
Max number of FPM servers would you like:
Reloading nginx configuration: nginx.
Restarting PHP5 FastCGI Process Manager: php5-fpm.
Site Created for with PHP support

The Script:

# @author: Seb Dangerfield
# Created:   11/08/2011
# Modified:   07/01/2012
# Modified:   27/11/2012
# Modify the following to match your system
# --------------END 
SED=`which sed`
CURRENT_DIR=`dirname $0`
if [ -z $1 ]; then
	echo "No domain name given"
	exit 1
# check the domain is valid!
if [[ "$DOMAIN" =~ $PATTERN ]]; then
	DOMAIN=`echo $DOMAIN | tr '[A-Z]' '[a-z]'`
	echo "Creating hosting for:" $DOMAIN
	echo "invalid domain name"
	exit 1 
# Create a new user!
echo "Please specify the username for this site?"
adduser $USERNAME
# -------
# CentOS:
# If you're using CentOS you will need to uncomment the next 3 lines!
# -------
#echo "Please enter a password for the user: $USERNAME"
#read -s PASS
#echo $PASS | passwd --stdin $USERNAME
echo "Would you like to change to web root directory (y/n)?"
if [ $CHANGEROOT == "y" ]; then
	echo "Enter the new web root dir (after the public_html/)"
	read DIR
# Now we need to copy the virtual host template
cp $CURRENT_DIR/nginx.vhost.conf.template $CONFIG
$SED -i "s/@@LOG_PATH@@/\/home\/$USERNAME\/_logs/g" $CONFIG
$SED -i "s#@@SOCKET@@#/var/run/"$USERNAME"_fpm.sock#g" $CONFIG
echo "How many FPM servers would you like by default:"
echo "Min number of FPM servers would you like:"
echo "Max number of FPM servers would you like:"
# Now we need to create a new php fpm pool config
cp $CURRENT_DIR/pool.conf.template $FPMCONF
$SED -i "s/@@HOME_DIR@@/\/home\/$USERNAME/g" $FPMCONF
chmod g+rx /home/$HOME_DIR
chmod 600 $CONFIG
# set file perms and create required dirs!
mkdir -p /home/$HOME_DIR$PUBLIC_HTML_DIR
mkdir /home/$HOME_DIR/_logs
mkdir /home/$HOME_DIR/_sessions
chmod 750 /home/$HOME_DIR -R
chmod 700 /home/$HOME_DIR/_sessions
chmod 770 /home/$HOME_DIR/_logs
chmod 750 /home/$HOME_DIR$PUBLIC_HTML_DIR
$NGINX_INIT reload
$PHP_FPM_INIT restart
echo -e "\nSite Created for $DOMAIN with PHP support"


Please note both downloads contain the same files, so you only need to download one.


Comments or Questions:

Please feel free to leave any comments or questions in the section below.

  • Love Nyberg

    Just what I been looking for. Great script.

    Thanks for sharing!

  • tony

    thanks for the wonderful script!

    just a correction note, the following line should be change:
    from: usermod -aG $USERNAME $WEB_SERVER_GROUP
    to: usermod -aG $WEB_SERVER_GROUP $USERNAME

    • Seb Dangerfield

      The line in the script is correct:


      The line adds the web server user(called group in this case, I should probably change that) to the new vhost users group. This allows any files owned by the new vhost users group to be read by the web server (i.e. css, js, images and so fourth…)

      If it was the other way around (as you’ve suggested) then the new vhost user would be added to the web servers group. This would allow the vhost user to read any files owned by the web server group, this would include the files of other vhosts and possibly other files only the web server user should have access to, i.e. it is a bad idea to do it this way round as it is less secure.

      • tony

        I see…in fact, I found out later that the line is also necessary for the web user group (eg. nginx) to access the php-fpm socket 🙂

        Lastly, while I was using the script, I noted that there might be a typo MAX_SERVERS+START_SERVERS (FPM_SERVERS?)

        Regardless, your script has been very helpful! Thanks again!

        • Michal

          Yes in my oppinion it is not working, there is no variable START_SERVERS only such think is in php pool.conf. So it should be MAX_CHILDS=$((MAX_SERVERS+FPM_SERVERS))

  • Your domain validation regex rejects domains beginning with digits, although they are now allowed by domain registrars. I modified the REGEX to be:


    Which works, but I am not a REGEX genius, so it is possible this may not be correct.

    Sites like and seem to break some RFCs, but not others.

    • Seb Dangerfield

      Good spot, I will update the regex shortly.

      UPDATE: regex updated!

  • Thanks for the great script! However there’s one little thing you may want to change, instead of create pid file in /var/run/php-fpm/”$USERNAME”_fpm.sock, just put it in /var/run/”$USERNAME”_fpm.sock or other place that is not ephemeral by design, otherwise it will cause fpm startup problem ([ERROR] bind() for address ‘/var/run/php-fpm/****_fpm.sock’ failed: No such file or directory) every time the server reboots.

    • Seb Dangerfield

      Good spot, I have updated the script now to put the socket file under /var/run

      This should solve any boot problems.

  • What’s the advantage of having a separate user per website? Why can’t they all just be nginx.

  • NicoVs

    David, the advantage is:
    – Security: users from other sites can’t hack into other sites
    – Load: Its easier to identiy which site is causing load on the server (if you have a shared server from multiple sites.

    • Seb Dangerfield

      Nicely put!

  • Hi does this cause extra memory or processing needs? All my customer sites are managed by myself with no client login allowed, would this be usefull still?

    • Seb Dangerfield

      For each site a new PHP-FPM pool of processes will be created, if you set each pool to a size of 5 and you had 10 sites then you would end up with 50 processes for handling PHP, it will also increase the memory usage due to larger number of separate processes been used to handle PHP requests. However it does help to increase the security of all the sites on the server as each site is owned and accessible to a different system user, it also allows you to easily see if one site is using up a lot more resources through PHP than other sites. Depending on the resources available on your server you would probably not want to run this configuration for more than about 10 – 20 sites on the same server.

  • You say at the beginning that you can further setup a chroot environment. Have you done this and if so how?

    • Seb Dangerfield

      I have setup php-fpm using the chroot option before however it really deserves a full article in it’s own right… But in a nutshell you set the the chroot option in the php-fpm config to the directory you want to use (make sure it’s an absolute path). Things to keep in mind:

      • All PHP paths will be relative to the chroot
      • Any third party libraries or binaries that your site depends on will need to be copied into the chroot
      • Make sure you access your DB via a port and not a socket as the directory where the socket lives will be outside the chroot environment
  • Thanks for this excellent tutorial. It is going to save me a lot of time setting up new domains. I’m fairly new to nginx so this really helps.

  • I’ve been spending a fair bit of time trying to figure out why this wasn’t working for me. I would create a site but when I tried to visit it in a browser I would get a 502 error. My error.log would read:

    [crit] 1435#0: *7 connect() to unix:/var/run/php-fpm/domain_fpm.sock failed (13: Permission denied) while connecting to upstream, client:, server: domain.tld, request: “GET / HTTP/1.1”, upstream: “fastcgi://unix:/var/run/php-fpm/domain_fpm.sock:”, host: “www.domain.tld”

    Turns out that the webserver user on my system is nginx, not www-data. So, I edited the WEB_SERVER_GROUP variable in your script to have nginx as its value (and actually I renamed the variable WEB_SERVER_USER, in line with previous comments 🙂 ).

    Everything is working now, as far as I know. Thanks for the script!

    Actually, one question: How should I configure my catch all domain in this case?

    • Seb Dangerfield

      Create a new vhost using the script and use or similar as the domain then go and edit the /etc/nginx/sites-available/ to change the server_name line and reload nginx.

  • Matt

    Awesome script. Noticed it’s not making the specific virtual host fpm_sockets however. So when the server restarts it can’t find the socket file defined in pool.d/host.pool.conf

    Any tip for this?

    • Seb Dangerfield

      This is probably because php-fpm is not set to start on boot. You can use chkconfig to easily ensure php-fpm is started when the server boots up.

  • Ad

    Does it also function in Archlinux?


    • Seb Dangerfield

      Yes it should work, you will just need to update the paths used in the script.

  • David

    Hi there!

    Thank you for this great solution. Is there any way to set-up SSL equal like with this script?

    I’m trying to offer my visitors the same content with SSL. I’m using the following code at the end of the

    server {
    listen 443 ssl;
    root "/home/www/public_html";

    ssl on;
    ssl_certificate /etc/ssl/private/;
    ssl_certificate_key /etc/ssl/private/;
    ssl_session_timeout 5m;

    ssl_protocols SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
    ssl_prefer_server_ciphers on;


    But it doesn’t work. Would be awesome if you could tell me how to get it working.


    • Seb Dangerfield

      You can only use one SSL certificate per port (i.e. one certificate for port 443) so unless all the sites are subdomains of the same domain and you have a wildcard ssl cert(i.e. a cert that covers * it won’t work. It is actually possible to have more than one ssl certificate per port but… you need to have SNI support enabled in nginx and it won’t work with older browsers. See the nginx docs for more info:

  • Steve

    Yes sure I’m using just one SSL certificate for I’ve dropped the code above into the same configuration file which has been created by the script.

    This should be the right place to paste it, isn’t it?

    Because if I do so, I can access the site via SSL only. If I don’t use it, I get a message called “File not found” for PHP files and a connection timeout for “normal” files. I think it’s because the listen values.

    Is there no solution to provide the website under 80 and 443?


  • Peter

    Anyway, I’ve got it working finally by following the instructions at

    Thank you a lot for your answer, I never would solve this problem without your help!


  • James

    Urrrghh.. I totally forgot to ask you if you could provide another script to remove domain with shell users and directories…

    Would be great!


    • Seb Dangerfield

      Good thinking, when I find a bit of spare time I will put one of these together.

  • Chris

    I found a bug btw.

    Steps to reproduce:
    Create something like /usr/share/nginx/error/404.html
    Make this 404 as your 404page by using the following code:

    error_page 404 /error/404.html;
    location /error {
    alias /usr/share/nginx/error;

    Now, you’ll notice, that this don’t work with these lines in your configuration file:

    location ~* \.(html|htm)$ {
    expires 30m;

    I’ve made the workaround by excluding HTML files from /error directory. But I’m sure there is a better way to handle with this.


    • Seb Dangerfield

      I’ll look into this, thanks for the bug report.

  • Ryan

    So useful. The only issue I had is restarting php-fpm. I don’t want my server going down even just for a few second each time I add a site. Any reload option for it like there is for nginx that will allow the new config to take place without any downtime?

  • Arndt

    Seems you didn’t update the regex yet?

    • Seb Dangerfield

      It has been updated now.

  • Kevin

    This is great! The only improvement I can think of would be to offer a version that runs on TCP/IP instead of sockets. Still, fantastic work!

  • Nikita Salnikov-Tarnovski

    I have a problem with this script. My Nginx on Ubuntu runs as user “nobody”, as a result it cannot connect to php socket, which is owned by user, created by provided script. What is the best to handle this situation?

    • Seb Dangerfield

      Simply modify the WEB_SERVER_GROUP= line at the top of the file to:


  • Mitesh Ashar

    I am maintaining the changes I am making to this script for my personal use, over here:

    You may choose to fork and make further changes. This will help us to maintain a repository with consistent identification of issues and addressing.

  • Benjamin

    Good so far, but how about logrotate?

    • Seb Dangerfield

      You will need to setup logrotate manually, however you can use wildcards in logrotate configs so you can easily setup one logfile definition for all log files for nginx.

  • lawrence

    thank u sir

  • Jonathan

    Thank you for this article !!!

    Could you please provide a way to create SSL certs as well while creating the vhost and to store them into /home/~domain/_ssl or so?

    • Seb Dangerfield

      This could certainly be added to the script, I will see if I get some time. However each vhost would need a separate IP address unless you were happy to only support browsers that support SNI.

  • What license is this under? Just in case I modify it and want to share it appropriately. (I would mention those changes here as well, of course…)

    Thanks for the script!

    • I’m asking since I’m moving from Apache to nginx and would like to copy some of your templates into my script.

    • Seb Dangerfield

      GPL version 3.0

  • It would be great if this would support creating subdomains that would add like a public_dev directory for under the same user… just a suggestion

    • Seb Dangerfield

      Good thinking Clark, I will see if I get time to add this.

  • Shannon Odom

    Would this lock a website from accessing below the site root /home/user/public_html? I just set this up, created a website and I a having issues because the configuration of the website requires access to:

    As well as a few other programs. Any insight to this situation would be greatly appreciated. I am relatively new to managing my VPS.

    • Seb Dangerfield

      I assume you are trying to get PHP to access files in /usr/bin … At the bottom of the PHP-FPM pool config file a PHP ini option called open_basedir is set to a select few directories if you add /usr/bin to this list and restart PHP-FPM then that will probably solve the problem. The open_basedir setting is used by PHP to decided whether or not it should allow methods like file_get_contents() to access a certain file.

  • Andre


    Amazing script! Congratulations. I just have two doubts and I’ll be happy if you could answer them:

    – Should I also use “disable_functions” in PHP to increase server security?
    – According to what you did, will users be always locked to their home directory? They will never be able to create a .php that access data from another folder, right?
    – Can I use it to have multiple websites in one server (like shared hosting) without facing any security issue? (I take privacy serious!)


    • Seb Dangerfield

      Yes using disable_functions is certainly a good idea. Users won’t be locked to the home directory if they SSH into the server or via SFTP (unless you jail them) and whilst it uses open_basedir to restrict access within PHP this doesn’t stop a malicious user using the exec command (or related) to step easily round this restriction. If your running a shared hosting server make sure users accessing the server by SSH/(S)FTP are jailed. Don’t allow vhost users access to other scripting language binaries such as python or perl (unless needed) and look into setting up SELinux.

      • Rick

        Interesting… So someone can run one of the php exec commands and that can skirt the permission settings? Can you elaborate on (or share a link to something on) how that happens, Seb? (Thanks for this post.)

  • krull


    Great script btw. Just one note.

    Since you’re setting a different session.path per php5-fpm conf file, the default garbage collector does not clean the expired session file, resulting in a very large _sessions folder.

    One suggestion is to also automatically create a cronjob daily per user that will handle the garbage collection.


    • Seb Dangerfield

      Good thinking, will look into that.

  • Andre


    Everytime I use the script I get (this is just an example):

    The process completes, but this message highlighted in blue says (translating to English): “chmod: impossible to access /home/*user-created*/: file or directory not found”.

    The site created works fine. But… Is it a bug in your code?

    • Seb Dangerfield

      It could either be the default location for users home directories is not /home or it could be the home directory is not getting created by default when the user is created.

  • Martin

    Hi Seb,

    can you provide an instruction, how to jail users?

    Also, have you new information about how to keep the session folders clean? Is there any solution for this?

    • cat /etc/php/fpm.d/vhost_1.conf
      chdir = /home/user/
      php_admin_value[open_basedir] = /home/user//public_html

  • Rostyslav

    Just say thanks!

  • Josh

    Do you happen to have the nginx.vhost.conf.template and pool.conf.template files you used handy? Would be nicer than figuring out exactly which directive matches a couple of the substitutes and hacking together my own file.

  • Thanks for posting this, I have a question regarding the user created.

    You mentioned in the bullet points “Creates a new system user for the site” but the script appears to just call “adduser $USERNAME” so I am assuming that you don’t actually mean that you are creating a system user, as in “–system” ( which defines a user below a particular UID number, have /bin/false as the shell, and be placed in the nogroup group by default) but just a user. I ask this because I am trying to determine if there is benefit or risk to running php-fpm under a system user which having the content of the site owned by a normal user. I always thought that it was good practice to have the user that owns the content be different from the web server that serves the content.

    I’m actually working on a multi-tenant setup using Apache and php-fpm but I think the concepts should be similar to that of nginx and php-fpm. That is the separation of users, groups, and processes per site.


  • I suggest you to create a user’s primary group “WEB_SERVER_GROUP”,
    it is much easier, should be something like this:
    cat /etc/passwd
    cat /etc/groups

    There is WEB_SERVER_GROUP 33=

    something like this useradd -g 33 USER -d /srv/http/USER

  • Pingback: nginx - userdir with user permissions - DL-UAT()

  • Pingback: Solution: User per virtual host in Nginx #answer #it #solution | SevenNet()

  • Pingback: Answer: User per virtual host in Nginx #programming #answer #development | Good Answer()

  • Pingback: How-to: User per virtual host in Nginx #solution #dev #development | IT Info()

  • zens

    i have 3 server,one is nginx server,ip=, one is mysql 5.7 server,ip=, one is php5.6 server,ip= , now in the nginx server i can open elinks but elinks ,it doesn’t work, it is An error occurred. how to configure the nginx and php server ,make tow server can combine the php server,thanks to tell me ,

  • Marek Miklewicz


    I have a problem with your script because when I create new domain and when I try to visit it I get 502 Bad Gateway and this is mi error log:

    2015/03/13 20:53:09 [crit] 2287#0: *14 connect() to unix:/var/run/www_fpm.sock failed (2: No such file or directory) while connecting to upstream, client: xx.xx.xx.xx, server:, request: “GET / HTTP/1.1”, upstream: “fastcgi://unix:/var/run/www_fpm.sock:”, host: “”

    and this is my virtualhost config file:

    server {
    root “/home/www/public_html”;

    index index.php;
    client_max_body_size 10m;

    access_log /home/www/_logs/access.log;
    error_log /home/www/_logs/error.log;

    if ($http_user_agent ~* (Baiduspider|webalta|nikto|wkito|pikto|scan|acunetix|morfeus|webcollage|youdao) ) {
    return 401;

    if ($http_user_agent ~* (HTTrack|clshttp|archiver|loader|email|harvest|extract|grab|miner) ) {
    return 401;

    location / {
    try_files $uri $uri/ /index.php$uri?$args;

    location ~ “^(.+\.php)($|/)” {
    fastcgi_split_path_info ^(.+\.php)(.*)$;

    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param SERVER_NAME $host;

    if ($uri !~ “^/uploads/”) {
    fastcgi_pass unix:/var/run/www_fpm.sock;
    include fastcgi_params;

    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
    expires max;
    log_not_found off;
    access_log off;

    location ~* \.(html|htm)$ {
    expires 30m;

    location ~* /\.(ht|git|svn) {
    deny all;

    can anyone help me?

  • Jonathan DeMasi

    Awesome script. Thanks so much for sharing! Definitely helpful.

  • thomas

    Just gets 403 Forbidden, looks like nginx does not have access to the files

  • Pingback: Unix/Linux:nginx – userdir with user permissions – Unix Questions()

  • Brent Wilson


    Can this be updated for Centos 7? A lot of the locations have changed, especially when you are using Remi repo for PHP 5.6

  • Can you upload it to github Thanks!

  • Pingback: CentOS 7: Magento 2.1 / NGINX / PHP 7 /PHP-FPM | Diary of A Lazy Tech()

  • Joep
  • Thomas

    Thanks for a great script, an updated version with option for Letsencrypt SSL would be great 🙂

  • Good evening everyone
    I have taken this wonderful work in the improvement the project is under github
    Participate to make it even more better