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:

Nginx:

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.


PHP-FPM

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):

include=/etc/php5/fpm/pool.d/*.conf

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 create_php_site.sh file to make it executable (if it isn’t already):

chmod u+x create_php_site.sh

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

./create_php_site.sh example.com

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:

./create_php_site.sh example.com
Creating hosting for: example.com
Please specify the username for this site?
example
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)?
y
Enter the new web root dir (after the public_html/)
web
How many FPM servers would you like by default:
2
Min number of FPM servers would you like:
1
Max number of FPM servers would you like:
5
Reloading nginx configuration: nginx.
Restarting PHP5 FastCGI Process Manager: php5-fpm.
 
Site Created for example.com with PHP support

The Script:

#!/bin/bash
# @author: Seb Dangerfield
# http://www.sebdangerfield.me.uk/?p=513 
# Created:   11/08/2011
# Modified:   07/01/2012
# Modified:   27/11/2012
 
# Modify the following to match your system
NGINX_CONFIG='/etc/nginx/sites-available'
NGINX_SITES_ENABLED='/etc/nginx/sites-enabled'
PHP_INI_DIR='/etc/php5/fpm/pool.d'
WEB_SERVER_GROUP='www-data'
NGINX_INIT='/etc/init.d/nginx'
PHP_FPM_INIT='/etc/init.d/php5-fpm'
# --------------END 
SED=`which sed`
CURRENT_DIR=`dirname $0`
 
if [ -z $1 ]; then
	echo "No domain name given"
	exit 1
fi
DOMAIN=$1
 
# check the domain is valid!
PATTERN="^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$";
if [[ "$DOMAIN" =~ $PATTERN ]]; then
	DOMAIN=`echo $DOMAIN | tr '[A-Z]' '[a-z]'`
	echo "Creating hosting for:" $DOMAIN
else
	echo "invalid domain name"
	exit 1 
fi
 
# Create a new user!
echo "Please specify the username for this site?"
read USERNAME
HOME_DIR=$USERNAME
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)?"
read CHANGEROOT
if [ $CHANGEROOT == "y" ]; then
	echo "Enter the new web root dir (after the public_html/)"
	read DIR
	PUBLIC_HTML_DIR='/public_html/'$DIR
else
	PUBLIC_HTML_DIR='/public_html'
fi
 
# Now we need to copy the virtual host template
CONFIG=$NGINX_CONFIG/$DOMAIN.conf
cp $CURRENT_DIR/nginx.vhost.conf.template $CONFIG
$SED -i "s/@@HOSTNAME@@/$DOMAIN/g" $CONFIG
$SED -i "s#@@PATH@@#\/home\/"$USERNAME$PUBLIC_HTML_DIR"#g" $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:"
read FPM_SERVERS
echo "Min number of FPM servers would you like:"
read MIN_SERVERS
echo "Max number of FPM servers would you like:"
read MAX_SERVERS
# Now we need to create a new php fpm pool config
FPMCONF="$PHP_INI_DIR/$DOMAIN.pool.conf"
 
cp $CURRENT_DIR/pool.conf.template $FPMCONF
 
$SED -i "s/@@USER@@/$USERNAME/g" $FPMCONF
$SED -i "s/@@HOME_DIR@@/\/home\/$USERNAME/g" $FPMCONF
$SED -i "s/@@START_SERVERS@@/$FPM_SERVERS/g" $FPMCONF
$SED -i "s/@@MIN_SERVERS@@/$MIN_SERVERS/g" $FPMCONF
$SED -i "s/@@MAX_SERVERS@@/$MAX_SERVERS/g" $FPMCONF
MAX_CHILDS=$((MAX_SERVERS+START_SERVERS))
$SED -i "s/@@MAX_CHILDS@@/$MAX_CHILDS/g" $FPMCONF
 
usermod -aG $USERNAME $WEB_SERVER_GROUP
chmod g+rx /home/$HOME_DIR
chmod 600 $CONFIG
 
ln -s $CONFIG $NGINX_SITES_ENABLED/$DOMAIN.conf
 
# 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
chown $USERNAME:$USERNAME /home/$HOME_DIR/ -R
 
$NGINX_INIT reload
$PHP_FPM_INIT restart
 
echo -e "\nSite Created for $DOMAIN with PHP support"

Download:

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

create_php_vhost.tar.gz
create_php_vhost.zip

Comments or Questions:

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

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

  1. 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

    • The line in the script is correct:

      usermod -aG $USERNAME $WEB_SERVER_GROUP
      

      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.

      • 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!

        • 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))

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

    PATTERN=”^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$”;

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

    Sites like 53.com and 10thhole.com seem to break some RFCs, but not others.

  3. 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.

  4. 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.

    • 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.

    • 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
  5. 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: 83.117.103.167, 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?

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

  6. 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?

    • 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.

  7. 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 example.com.conf:


    server {
    listen 443 ssl;
    server_name example.com;
    root "/home/www/public_html";

    ssl on;
    ssl_certificate /etc/ssl/private/example.com-CRT;
    ssl_certificate_key /etc/ssl/private/example.com-KEY;
    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.

    Cheers
    Ben

    • 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 *.example.com) 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: http://nginx.org/en/docs/http/configuring_https_servers.html#sni

  8. Yes sure I’m using just one SSL certificate for sub.example.com. 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?

    Cheers
    Max

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

    Would be great!

    Cheers
    Samuel

  10. 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.

    Cheers
    Anton

  11. 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?

  12. 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!

  13. 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?

    • 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.

  14. 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?

    • 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.

  15. 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:
    /usr/bin/lame
    /usr/bin/ffmpeg
    /usr/bin/mediainfo

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

    • 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.

  16. Seb,

    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!)

    Thanks.

    • 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.

  17. seb,

    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.

    FYI.

    • 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.

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=""> <strike> <strong>