Setting up PHP with FastCGI and suexec on Centos 5.5

http://blog.giantchipmunk.com/2011/01/setting-up-php-with-fastcgi-and-suexec.html

I am setting up a shared web host in a clean Centos 5.5 installation. The advantages of using this setup versus the default mod_php include:

  • PHP scripts run as the user who owns the site, not apache. A user will not be able to use PHP scripts to browse the website source files of other users.
  • Files created by PHP software running on the webserver will be owned by the users, not apache

The main advantage of using FastCGI over regular CGI + suexec is performance. CGI spawns and tears down an instance of PHP every time a script is loaded, while FastCGI reuses PHP processes to service multiple requests.

Setting up users

Assume that user1 will be creating a website, located at user1.com. I like to use user private groups, so I create a user as such:

groupadd user1
useradd -g user1 user1

The documentroot for user1.com will be in /home/user1/websites/user1.com/www, and logs will be in /home/user1/websites/logs. Create them:

mkdir -p /home/user1/websites/user1.com/www
mkdir /home/user1/websites/logs
chown -R user1:user1 /home/user1/websites

 

Installing mod_fcgid

The default CentOS 5.5 repositories do not have mod_fcgid for Apache, so we will have to add a non-CentOS repository. I like to use repositories that are maintained by organizations, so I decided to go with EPEL (Extra Packages for Enterprise Linux). Repos are more convenient than compiling from source, especially when dealing with updates. To prevent overwriting of official CentOS packages, I use Yum Priorities to keep things straight.

First, let’s update our system:

yum update

Then, install Yum Priorities:

yum install yum-priorities

Edit /etc/yum.repos.d/CentOS-Base.repo and set all of the Centos repos to priority=1. Then download and install the rpm for EPEL:

wget http://download.fedora.redhat.com/pub/epel/5/i386/epel-release-5-4.noarch.rpm
rpm -i epel-release-5-4.noarch.rpm

There will now be an epel.repo file in /etc/yum.repos.d. Edit epel.repo and set each repository’s priority to 11.

Now we can install mod_fcgid. As an added bonus, let’s also grab eaccelerator for PHP:

yum update
yum install mod_fcgid
yum install php-eaccelerator

 

Setting up FastCGI for PHP

There should now be an fcgid.conf file in /etc/httpd/conf.d. Restart Apache to make sure that it loads correctly:

service httpd restart

We need to create a FastCGI wrapper for each user. Since we will be using suexec, the wrapper needs to be in a certain directory. Run:

suexec -V

AP_DOC_ROOT should show “/var/www”. Our wrappers need to be below that directory.

mkdir /var/www/php-fcgi-scripts
mkdir /var/www/php-fcgi-scripts/user1

Create a file named php-fcgi-starter in php-fcgi-scripts/user1 containing:

#!/bin/sh
PHPRC=/etc
export PHPRC
export PHP_FCGI_MAX_REQUESTS=500
export PHP_FCGI_CHILDREN=4
exec /usr/bin/php-cgi

PHP will spawn 4+1=5 children (per user account), each serving up to 500 requests (limited just in case there are memory leaks). The PHPRC variable tells PHP to look for php.ini in /etc.

The wrapper script and its directory need to be owned by user1. Additionally, the script should be executable:

chown -R user1:user1 /var/www/php-fcgi-scripts/user1
chmod +x /var/www/php-fcgi-scripts/user1/php-fcgi-starter

There is one line we need to add to the php configuration file. Add this line to the bottom of /etc/php.ini:

cgi.fix_pathinfo = 1

Since we are no longer using mod_php, disable it by renaming the apache .conf file:

cd /etc/httpd/conf.d
mv php.conf php.conf.disabled

The php.conf file adds index.php as a directory index file. We want to keep the same behavior, so add this line to /etc/httpd/conf.d/fcgid.conf:

DirectoryIndex index.php

Finally, we add the VirtualHost configuration to /etc/httpd/conf/httpd.conf (or a .conf file in /etc/httpd/conf.d):

LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" vcombined

NameVirtualHost *:80

<VirtualHost *:80>
  DocumentRoot /home/user1/websites/user1.com/www
  ServerName user1.com
  ServerAlias www.user1.com
  CustomLog /home/user1/websites/logs/access_log vcombined
  ErrorLog /home/user1/websites/logs/error_log

  <IfModule mod_fcgid.c>
    SuexecUserGroup user1 user1
    PHP_Fix_Pathinfo_Enable 1
    <Directory /home/user1/websites/user1.com/www>
      Options +ExecCGI
      AllowOverride All
      AddHandler fcgid-script .php
      FCGIWrapper /var/www/php-fcgi-scripts/user1/php-fcgi-starter .php
      Order allow,deny
      Allow from all
    </Directory>
  </IfModule>
</VirtualHost>

The “vcombined” log format helpfully adds the virtual host name to the log file. If you don’t need it, you can omit the LogFormat line and use “combined”. Copy and edit the VirtualHost section for each additional website. Now restart Apache and test:

service httpd restart

 

User-specific php.ini files

Say user1 stores personal files in his home directory outside of ~/websites, and he is paranoid about security holes in his PHP scripts that may allow outside access of those files. Open_basedir is a PHP variable that can prevent scripts from reading files outside of a specified directory. We will specify this in a user-specific php.ini file.

First, copy php.ini to the user’s website directory:

cp /etc/php.ini /home/user1/websites
chown user1:user1 /home/user1/websites/php.ini

Edit /home/user1/websites/php.ini and set open_basedir to /home/user1/websites. Then, edit /var/www/php-fcgi-scripts/user1/php-fcgi-starter and change PHPRC to /home/user1/websites.