The basic configuration I’ll use as an example is rather simple:

server {
listen 80 default_server;
root /var/www/html;
index index.php index.html;

location / {
}

location /dotclear {
alias /var/www/dotclear;
autoindex on;
}

location /davical {
alias /usr/share/davical/htdocs;
autoindex on;
}
}

At this stage, PHP is not enabled, yet. What we have here is a main site that contains some simple PHP files; a Dotclear installation that is aliased to the “/dotclear” location, and also uses PHP files; and a DAViCal installation that is aliased to the “/davical” location, and uses PHP files as well. The latter two locations also use “path info”, a way to have cleaner URIs by organizing parameters in a kind of pseudo-directory-hierarchy instead of having them in a disorganized ?param1&param2&… notation.

The general recommendation from Nginx users is to use the “root” directive instead of “alias” whenever possible, especially where PHP is concerned, all the more if path info is used. While this recommendation can be applied to “/dotclear” (because the directory name matches the URI base-name), it clearly cannot be applied to “/davical” (no match), much less to a child location of “/davical” that is not shown here (for CalDavZAP) since I wouldn't want to drop PHP files inside the /usr directory, that shouldn’t be changed in any other way than through the Linux distribution’s package manager.

My goal was to enable PHP by defining the rules for handling PHP once and for all, with at most a sub-location and one line to add to each of the locations above.

My first working solution did enable PHP for all of the above, but unfortunately it did not enable the use of path info. Still, I’ll show it here, because it is really more elegant than the full-featured solution, and probably more efficient too. Besides, “path info” is not always needed. This solution involves two files:
  • the main configuration file:
    server {
    listen 80 default_server;
    root /var/www/html;
    index index.php index.html;

    location / {
    include php.fast.conf;
    }

    location /dotclear {
    alias /var/www/dotclear;
    autoindex on;
    include php.fast.conf;
    }

    location /davical {
    alias /usr/share/davical/htdocs;
    autoindex on;
    include php.fast.conf;
    }
    }
  • an additional configuration file named /etc/nginx/php.fast.conf:
    location ~ \.php$ {
    fastcgi_split_path_info ^(.+?\.php)(/.*)?$;
    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $request_filename;
    }

This is working because the PHP part is evaluated anew for each new context. For completeness, I should add that Debian up to Wheezy redefines SCRIPT_FILENAME to $request_filename instead of the Nginx default $document_root$fastcgi_script_name. As the Nginx default doesn’t work with aliases, I could work with the value set by Debian and the last line above was not needed. Since Debian Jessie, this line must be present.

Path info doesn’t work, though, because the rexex “\.php$” ends with “$”, which means that there can be nothing after the php extension. Unfortunately, changing “$” to “(/|$)” is not a solution because then, even though URIs with a path info do match, the full URI is given to the PHP engine, and PHP complains that the given URI does not point to a PHP file:

2013/10/27 11:00:22 [error] 20220#0: *12 FastCGI sent in stderr: "Access to the script '/usr/share/davical/htdocs/caldav.php/shared/meetings.ical/' has been denied…

Such an error message could be avoided by reverting the PHP property “cgi.fix_pathinfo” to its initial value of “1”. But this would be a security risk, as explained at many places by the Nginx community; a risk I’m not willing to take.

So I switched to my final solution below, all in a single file:

server {
listen 80 default_server;
root /var/www/html;
index index.php index.html;

location / {
rewrite ^(/.*?\.php)(/.*)?$ /...$document_root/.../...$1/...$2 last;
}

location /dotclear {
alias /var/www/dotclear;
autoindex on;
rewrite ^(/dotclear)(/.*?\.php)(/.*)?$ /...$document_root/...$1/...$2/...$3 last;
}

location /davical {
alias /usr/share/davical/htdocs;
autoindex on;
rewrite ^(/davical)(/.*?\.php)(/.*)?$ /...$document_root/...$1/...$2/...$3 last;
}

location /... {
internal;
autoindex off;
location ~ ^/\.\.\.(?<p_doc_root>.*)/\.\.\.(?<p_prefix>.*)/\.\.\.(?<p_script>.*\.php)/\.\.\.(?<p_pathinfo>.*)$ {
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $p_doc_root$p_script;
fastcgi_param SCRIPT_NAME $p_prefix$p_script;
fastcgi_param REQUEST_URI $p_prefix$p_script$p_pathinfo$is_args$query_string;
fastcgi_param DOCUMENT_URI $p_prefix$p_script$p_pathinfo;
fastcgi_param DOCUMENT_ROOT $p_doc_root;
fastcgi_param PATH_INFO $p_pathinfo if_not_empty;
#fastcgi_param PATH_TRANSLATED $p_doc_root$p_pathinfo;
}
}
}

This solution works by emulating a kind of “php-fpm” function (the last location above), that takes 4 parameters (“?<p_…>” above), with the string “/...” as a separator:

  1. first the file-system prefix, i.e. the place where web files are stored;
  2. then the URI prefix, i.e. the web location under which web pages are found;
  3. then the URI ending, written so that it can be appended both to the first parameter (to obtain the actual PHP file’s full path), and to the second (to obtain the full URI minus the parameters);
  4. finally the path info, if any was detected in the URI.

This “function” is called by the rewrite rules; a simple copy–paste is enough to create a new call for a new location, the only part to adapt being the location at the start of the rewrite rule. The only different rewrite rule is the one for locations that work on a “root” instead of an “alias”, because then the location’s path is supposed to be appended to the “root” to find the PHP file, instead of being substituted by the “alias”; such a rewrite rule thus has one less “parameter” to give to the “function”.

Note the important “internal” keyword inside the “function”: it ensures that for example “http://myserver/info.php” works but “http://myserver/.../var/www/html/.../.../info.php/...” does not; without the “internal” keyword, it would, and it would be a security issue.

Also note that I did not define the PATH_TRANSLATED variable as found in numerous tutorials, because first, I noticed that the standard Debian configuration for PHP, from which my first solution is derived, does not define it; and second, defining this variable kept PHP from having the PHP_SELF variable properly defined, for some reason.
On the other hand, I did define the PATH_INFO variable, even though the standard Debian configuration does not, because some applications need it, like DAViCal. I had to append the “if_not_empty” keyword to the variable declaration, though, else the PHP_SELF variable becomes undefined again.

Some enhancements:

  • The whole “location /... { … }” block can be stored in a /etc/nginx/php.full.conf file, which makes the main file smaller, thus easier to read, and allows this block to be included by several servers (for example the HTTP and HTTPS servers).
  • The two methods I described are not incompatible with one-another. It is thus possible to use the fastest one in most cases, and point to the full PHP setup only when the path info feature is needed.

Here is the same Nginx configuration as before, when applying both enhancements:

server {
listen 80 default_server;
root /var/www/html;
index index.php index.html;
include php.full.conf;

location / {
include php.fast.conf;
}

location /dotclear {
alias /var/www/dotclear;
autoindex on;
rewrite ^(/dotclear)(/.*?\.php)(/.*)?$ /...$document_root/...$1/...$2/...$3 last;
}

location /davical {
alias /usr/share/davical/htdocs;
autoindex on;
rewrite ^(/davical)(/.*?\.php)(/.*)?$ /...$document_root/...$1/...$2/...$3 last;
}
}

That’s all. I hope it will be of some help to some people. It was definitely hard to come by this final solution!

Changelog:

  • 2013-10-29 — PATH_INFO is actually needed; only PATH_TRANSLATED is harmful. Some enhancements.
  • 2013-10-30 — Helping MrHaroldA on IRC (#nginx @ freenode.net) made me realize that part of the rewrite regex should be non-greedy.
  • 2013-10-31 — kolbyjack on IRC (#nginx @ freenode.net) suggested that “($|/.*$)” was more elegant like this: “(/.*)?$” (as I did for the fast solution, by the way); he is right!
  • 2013-11-26 — Some clarifications related to Debian, and some more explanations.
  • 2015-05-03 — Debian Jessie changed the value of SCRIPT_FILENAME.