Blog hébergé par Yves et Iris :-)

Aller au contenu | Aller au menu | Aller à la recherche

Configure PHP with Nginx only once for several aliases

When I first installed Nginx, I discovered that PHP configuration was much less straightforward than it was with Apache. In my case, PHP is used both on the main “root” site, and with several “alias” locations. Besides, some of these locations use a feature called “path info” that definitely added some spice to the challenge!

While this article was written in the context of Debian Linux Wheezy, I’m sure it is not very different with other distributions.

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.

Commentaires

1. Le samedi 29 mars 2014, 19:02 par Pieter Jordaan

This is the first tutorial I found that actually works and makes sense. Thank you very much!

2. Le mercredi 23 avril 2014, 08:55 par theYinYeti

Thank you Pieter for your comment. I wish to help, and I'm glad to see that this post actually does :-)

3. Le jeudi 18 septembre 2014, 18:20 par Ihsan

I've never made it work when using aliases. Now it simply works. You helped me a lot. Thanks!

4. Le vendredi 8 mai 2015, 12:05 par Martin Rüegg

great solution! thanks!

i found out that reqest to /dotclear.somting_else or /dotclearisonlythebeginning where also catched by the directive. so i added the following to the server{} directive:

location = /dotclear {
rewrite ^/dotclear$ /dotclear/ last;
}

additionally i changed the doclear-alias to:

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

* a) note the "^~ " at the beginning of location (so everything starting with "/dotclear/" is captured unconditonally)
* b) note the "/" at the end of location AND alias. (in order to make it distinct from e.g. /dotclear.something)
Btw: on windows, the directory separater is best used as forward-slash as well to avoid errors when alias ends with such.

regards,
martin.

5. Le lundi 1 juin 2015, 14:51 par theYinYeti

Thank you for these tips, Martin! I'm sure my readers will appreciate. Indeed, this page is one of the most read from my little-known blog ;-)

6. Le samedi 27 février 2016, 11:06 par dede

This definately is the BEST page on nginx and aliases. I've tried so many configurations and searched a long time - but this one is just working! Great!
Everybody who has issues on setting up nginx with alias and fastcgi php should read this.
Thank you!

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.

La discussion continue ailleurs

URL de rétrolien : http://yalis.fr/cms/index.php/trackback/21

Fil des commentaires de ce billet