PHP-FPM, application server made by PHP

Jean Baptiste FAVRE

November 2010

La version Française est disponible ici: PHP-FPM, le serveur d'application made by PHP.

Introduction

Before, running different PHP applications on the same web server was somewhat hard to secure. Basically, you had only 2 options::

Nowadays, you can use PHP-FPM. You all know PHP, but not all of you know FPM. FPM stands for "FastCGI Process Manager", it allow you manage PHP process with some interesting features:

Using PHP-FPM, you'll be able to reach a good isolation level for your PHP apps. I'm not going to deeply analyze PHP security features. Instead, I'll just explain a way to get PHP-FPM working.

Preparation

I won't explain in detail howto install different components. Others have already done it, much more better then I could.

We're going to see, in a practical way, how to make the best use of PHP-FPM features to deploy and setup PHP apps:

chroot setting is not easy. During my tests, I figure that one of my app wasn't able to perform DNS resolution. For sure, one library was missing, so take care of it if you want to try.

Anyway, we need a basic directory tree, whatever could the future app be. Let's say web root is /var/www/. Then, hosted domains will be in /var/www/domains/, mutualized apps in /var/www/apps/.

For more details, you can refer to my previous publications concerning Apache or Nginx (only in french for the time being, sorry):

As a summary, here is what our structure looks like for domain domain.tld:

tree /var/www/domains/
domain.tld/
|-- config
|   |-- auth
|   `-- ssl
|-- logs
|   |-- access.log
|   |-- error.log
`-- docroot
    |-- subdir1
    |-- subdir2
    |-- subdir3
    `-- subdir4

For a virtual app appname, no big difference:

tree /var/www/apps/
appname/
|-- config
|-- docroot
|-- logs
|-- private
`-- tmp

Some details:

PHP-FPM installation

No surprise here, I use the excellent work from Guillaume Plessis, DotDeb maintainer.

for Debian
cat /etc/apt/sources.list
deb //php53.dotdeb.org stable all
deb //packages.dotdeb.org stable all
gpg --keyserver keys.gnupg.net --recv-key 89DF5277
gpg -a --export 89DF5277 | sudo apt-key add -
aptitude update
aptitude install php5-fpm

Once installed, it's time to begin configuration. PHP-FPM will start a root process which will manage some pools. Theses pools will run under specified UID/GID, all parameters we are about to define.

PHP-FPM configuration

Under Debian, you'll have to modified file /etc/php5/fpm/php5-fpm.conf, like this:

cat /etc/php5/fpm/php5-fpm.conf
;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;

[global]
pid = /var/run/php5-fpm.pid

error_log = /var/log/php5-fpm.log
log_level = notice

emergency_restart_threshold = 5
emergency_restart_interval = 2
process_control_timeout = 2

daemonize = yes

;;;;;;;;;;;;;;;;;;;;
; Pool Definitions ;
;;;;;;;;;;;;;;;;;;;;
include=/home/hosting/www/apps/*/config/fpm-pool.conf
include=/home/hosting/www/domains/*/config/fpm-pool.conf

As you can see, global config use only few options. The real stuff stands in the last 2 lines which allow:

Sample domain pool

First pool to be configured is a domain pool. It'll run with specific UID/GID. By default, it'll be chrooted so that it can only see domain directory tree. Finally, some PHP configuration options will be overwirtten

Domain pools will use UID between 9001 and 9999. That will also be the TCP port which pool will listen on, waiting for requests.

cat /var/www/domains/domain.tld/config/fpm-pool.conf
[domain.tld]
    listen                 = 127.0.0.1:9001
    listen.backlog         = -1
    listen.allowed_clients = 127.0.0.1
    listen.owner           = domain.tld
    listen.group           = domain.tld
    listen.mode            = 0666

    user  = domain.tld
    group = domain.tld

    pm                   = dynamic
    pm.max_requests      = 0
    pm.max_children      = 2
    pm.start_servers     = 1
    pm.min_spare_servers = 1
    pm.max_spare_servers = 1

    pm.status_path       = /php_pool_domain.tld_status
    ping.path            = /domain.tld_ping
    ping.response        = domain.tld_pong
 
    request_terminate_timeout = 2
    request_slowlog_timeout   = 1
    slowlog                   = /var/www/domains/domain.tld/logs/php-slow.log
 
    ;rlimit_files = 1024
    ;rlimit_core = 0
 
    chroot = /var/www/domains/domain.tld/
    ; Chdir to this directory at the start. This value must be an absolute path.
    ; Default Value: current directory or / when chroot
    chdir = /docroot/

    catch_workers_output = yes
 
    env[HOSTNAME] = $HOSTNAME
    env[TMP]      = /tmp
    env[TMPDIR]   = /tmp
    env[TEMP]     = /tmp

    ;   php_value/php_flag             - you can set classic ini defines which can
    ;                                    be overwritten from PHP call 'ini_set'. 
    ;   php_admin_value/php_admin_flag - these directives won't be overwritten by
    ;                                     PHP call 'ini_set'
    php_flag[display_errors]            = on
    php_admin_value[error_log]          = /logs/php_err.log
    php_admin_flag[log_errors]          = on
    php_admin_value[memory_limit]       = 1M
    php_value[max_execution_time]       = 2

Some explanations:

You may ask why slowlog options still use non-chrooted path. Quite simple: long running scripts detection is made by root PHP process and this process is not chrooted. You could centralise slowlogs into one file, but having it into separate give the webmaster the oportunity to have a look on them if he has a FTP or SSH access, even limited.

OK, now we've configured our pool, let's create associated user domain.tld:

User creation for specific app
mkdir -p /var/www/domains/domain.tld/{config,docroot,logs,stats,tmp}
addgroup --force-badname --system --gid 9001 domain.tld
adduser --home /var/www/domains/domain.tld --shel /bin/false --no-create-home \
     --uid 9001 --gid 9001 --force-badname --disabled-password --disabled-login domain.tld

You notice --force-badname use. It's mandatory if you want to use dot in username, which is somewhat usefull as username should reflect domain name. I did not see any side effect for now, but feel free to do it another way.

Sample application pool

This sort of pool will run with specific identity. By default, it will also be chrooted so that it can only access its directory tree. Finally, some of the PHP parms will have dedicated values for this pool.

Applications shared between domains will use gid/uid between 10001 and 10999. This value will also be PHP-FPM TCP port.

cat /var/www/apps/appname/config/fpm-pool.conf
[appname]
    listen                 = 127.0.0.1:10001
    listen.backlog         = -1
    listen.allowed_clients = 127.0.0.1
    listen.owner           = appname
    listen.group           = appname
    listen.mode            = 0666

    user  = appname
    group = appname

    pm                   = dynamic
    pm.max_requests      = 0
    pm.max_children      = 10
    pm.start_servers     = 5
    pm.min_spare_servers = 5
    pm.max_spare_servers = 5
    pm.status_path       = /php_pool_appname_status

    ping.path     = /appname_ping
    ping.response =  appname_pong
 
    request_terminate_timeout = 5
    request_slowlog_timeout   = 2
    slowlog                   = /var/www/apps/appname/logs/php-slow.log
 
    ;rlimit_files = 1024
    ;rlimit_core = 0
 
    chroot = /var/www/apps/appname/
    chdir = /docroot/

    catch_workers_output = yes
 
    env[HOSTNAME] = $HOSTNAME
    env[TMP]      = /tmp
    env[TMPDIR]   = /tmp
    env[TEMP]     = /tmp

    ;   php_value/php_flag             - you can set classic ini defines which can
    ;                                    be overwritten from PHP call 'ini_set'. 
    ;   php_admin_value/php_admin_flag - these directives won't be overwritten by
    ;                                     PHP call 'ini_set'
    php_flag[display_errors]       = on
    php_admin_value[error_log]     = /logs/php_err.log
    php_admin_flag[log_errors]     = on
    php_admin_value[memory_limit]  = 2M

Nothing special here. All has already been explained at previous step. Once more, slowlog option is not impacted by chroot. Of course, you have to create the user which will be used:

User account creation
mkdir -p /var/www/apps/appname/{config,docroot,logs,private,tmp}
addgroup --force-badname --system --gid 9001 domain.tld
adduser --home /var/www/apps/appname --shel /bin/false --no-create-home \
     --uid 9001 --gid 9001 --disabled-password --disabled-login appname
chmod 7777 /var/www/apps/appname/tmp
chown appname: /var/www/apps/appname/private
chmod -R u=rwX,g=rX,o= /var/www/apps/appname/private

Well. We now have installed PHP-FPM, setted up the root process, setted up 2 pools, one for domain and the other one for a shared application.It's now time to configure NGinx so that they could be reachable.

Nginx configuration

I won't explian nginx installation, as it's really trivial under Debian. Eventually, you'll have little troubles to get backport version installed instead of stable one. As usual, Google is your best friend ever !

NGinx will be configured as follow:

Default vhost with pool's monitoring URLs
Even if chrooted, it's always a good idea to monitor things.
URLs entry points for your pools.
Theses URLs definition, using Location in Nginx, will be included in each impacted vhost.
NGinx configuration
server {
	listen 80 default;
	allow 127.0.0.1;
	deny all;
	location = /nginx_status {
		stub_status on;
		access_log off;
		allow 127.0.0.1;
		deny all;
	}
	include /var/www/domains/*/config/nginx_status.conf;
	include /var/www/apps/*/config/nginx_status.conf;
}
server {
        listen 80;
        server_name domain.tld *.domain.tld;
        root /var/www/domains/domain.tld;

        access_log /var/www/domains/domain.tld/logs/access.log vhosts;
        error_log  /var/www/domains/domain.tld/logs/error.log info;

        location / {
            root   /var/www/domains/domain.tld/docroot/;
            try_files $uri $uri/ $uri.php;
        }

        ##### Enabled domain.tld PHP support #####
        include /var/www/domains/domain.tld/config/nginx.conf;

        ##### Enabled global PHP apps support #####
        include /var/www/apps/appname/config/nginx.conf;
        # 1 line per PHP app to enable...
        #include /var/www/apps/appname1/config/nginx.conf;
        #include /var/www/apps/appname2/config/nginx.conf;
}

As you can see, default chost will load each monitoring URL (file nginx_status.conf) for each PHP-FPM pool. "domain.tld" vhost will load each enabled app configuration file.

For applications, nothing strange or difficult, at least for those how already speak NGinx syntax:

Application pool configuration
location = /php_pool_appname_status {
	fastcgi_pass 127.0.0.1:10001;

	##### Include common FastCGI params #####
	include /etc/nginx/conf.d/fastcgi.conf;
}
location ~ /appname(.*)\.(css|js|gif|png)$ {
    root /var/www/apps/appname/docroot;
    access_log off;
    expires 30d;
}
location /appname {
    fastcgi_split_path_info ^(.+\.php)([^?]*).*$;
    # A handy function that became available in 0.7.31 that breaks down 
    # The path information based on the provided regex expression
    # This is handy for requests such as file.php/some/paths/here/ 

    fastcgi_pass   127.0.0.1:10001;

    fastcgi_connect_timeout          5;
    fastcgi_send_timeout             5; 
    fastcgi_read_timeout             5; 
    fastcgi_buffer_size              128k;
    fastcgi_buffers                4 256k;
    fastcgi_busy_buffers_size        256k;
    fastcgi_temp_file_write_size     256k;
    fastcgi_intercept_errors         on;

	##### Include common FastCGI params #####
    include /etc/nginx/conf.d/fastcgi.conf;
}
Domain pool configuration
location = /php_pool_domain.tld_status {
	fastcgi_pass 127.0.0.1:9001;

	##### Include common FastCGI params #####
	include /etc/nginx/conf.d/fastcgi.conf;
}
location ~ \.php$ {
	fastcgi_split_path_info ^(.+\.php)([^?]*).*$;
	# A handy function that became available in 0.7.31 that breaks down
	# The path information based on the provided regex expression
	# This is handy for requests such as file.php/some/paths/here/

	fastcgi_pass   127.0.0.1:9001;
	# Forward request to "domain.tld" pool

	fastcgi_connect_timeout          1;
	fastcgi_send_timeout             1;
	fastcgi_read_timeout             2;
	fastcgi_buffer_size              128k;
	fastcgi_buffers                4 256k;
	fastcgi_busy_buffers_size        256k;
	fastcgi_temp_file_write_size     256k;
	fastcgi_intercept_errors         on;

	include /etc/nginx/conf.d/fastcgi.conf;
}

Now you need common FastCGI configuration file:

FastCGI configuration
fastcgi_index  index.php;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
#SCRIPT_NAME needs to remain without the document_root on front
#Otherwise PHP_SELF breaks, the interpreter will still find the file
#because of the added SCRIPT_FILENAME parameter below

fastcgi_param  SCRIPT_FILENAME    $fastcgi_script_name;
#This one was added right into the file, since with root moved, and path_info split
#It should no longer need to be set manually for each virtual host

fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

#the two lines below are possible now because of the fastcgi_split_path_info function
fastcgi_param  PATH_INFO          $fastcgi_path_info;
fastcgi_param  PATH_TRANSLATED    $document_root$fastcgi_path_info;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

#PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

Tests

Once done, it's time to test. Just create sample "phpinfo" file here: /var/www/domains/domain.tld/docroot/index.php and another one here: /var/www/apps/appname/docroot/index.php with following content:

index.php test file
<?php
phpinfo();

Then, check with you browser following URLs:

Domain pool
//domain.tld/
Application pool
//domain.tld/appname/

You should see the well known PHPInfo. If not then... have a break and take back from the beginning ;-)

About Jean Baptiste FAVRE

I spend most of my free time on the Internet working on GNU/Linux with Debian or CentOS, virtualization with Xen and KVM technology, as well as cluster stacks with corosync and OpenAIS. Particularly interested in Linux, Netfilter, virtualization, monitoring and clusters, most of my personal works are published on this website and others should not delay. By way professional, I manage servers running RedHat or CentOS and VMware ESXi farm.
From time to time, I manage to drop my keyboard and read a book while listening to music, but it never lasts long.

License

Creative Commons License This document is published under Creative Common by-nc-sa license.

Valid XHTML 1.0 Strict |  Valid CSS |  license Creative Common by-nc-sa

Table des matières

  1. Introduction
  2. Preparation
  3. PHP-FPM installation
  4. PHP-FPM configuration
  5. Sample domain pool
  6. Sample application pool
  7. NGinx configuration
  8. Tests
  9. About ...
  10. License