NGINX PHP-FPM essentials
When you configure NGINX against PHP-FPM, what you really do is teach NGINX where is PHP-FPM listening and what kind of information has to be delivered to it.
NGINX does not do any processing of PHP scripts of its own. It merely talks to PHP-FPM and says:
“Hey PHP-FPM, please run
</path/to/this.php>
and tell me what you see”.
This couple works well together. On a website with SEO-friendly URLs, NGINX does the job of rewriting request URL in order to construct and pass the proper filename to PHP-FPM. And PHP-FPM does the heavy lifting of parsing the script, running it and delivers resulting HTML back to NGINX.
In general, aside from some extra bits of information like environment variables, NGINX delivers one important piece of information to PHP-FPM – the filename of the script.
The standard bit of configuration illustrates it:
location ~ \.php$ {
# where is PHP-FPM listening? the socket
fastcgi_pass unix:/path/to/php-fpm.sock;
# everytime we're in this location, tell PHP-FPM the complete script filename to be executed
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # <========
# and don't forget to tell PHP-FPM stuff like SERVER_NAME:
include fastcgi_params;
NGINX does not even need to have read access to the .php file in question in order to have it properly launched via PHP-FPM and then displayed to clients.
So this chmod in your site setup may be perfectly valid:
-- site ----- index.php (chmod 0400, chown user:user) ----- style.css (chmod 0640, chown user:nginx)
Note how NGINX has no access for index.php
whatsoever, yet the page will work just fine in our setup.
Let’s expand to another case, our case of interest for this post.
What would happen when someone visits a non-existent .php
file? NGINX will happily pass it along to PHP-FPM as usual, and PHP-FPM will return the dreaded:
No input file specified.
This error is emitted by NGINX to the client’s browser. Because that’s what PHP-FPM happens to produce when you give it a filename that doesn’t exist.
Well, it’s a 404 error. Why bother? Sure enough, that’s not really the kind of error you want your users to see.
So in this post, I will tell you what are the dos and don’ts in fixing this error. That is, aside from obvious misconfiguration you might have.
Our case is when the script file is really not there. In other words, let’s see what is the best configuration approach to handling 404s for requests to missing .php files. And showing something better looking than No input file specified
.
Solution on the PHP-FPM side? None that I know
There’s no way to customize the message. Well, in theory, you can patch and recompile PHP like those poor folks who like to play with NGINX compilation.
So let’s move on to NGINX. There are multiple ways to solve it there, and I tell you this – there are too many ways to do things inherently wrong. Use the power that NGINX gives you wisely.
Solutions in NGINX
Going down from worst to best.
Worst
You may have seen this one:
location ~ \.php$ {
if (!-e $request_filename) { rewrite / /index.php last; } ## Catch 404s that try_files miss
...
fastcgi_pass unix:/path/to/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
The idea is to check if .php script is really there, then rewrite to /index.php
, which is supposed to know how to display 404 nicely (some CMS framework). Cute, nah?
You know why it’s already bad: if
is evil. We all already know that blah blah.. Give me a break 🙂
But what would happen if we retain our very secure chmod (0400
on .php files) that we began our post with? We’ll get error upon accessing any .php
file:
rewrite or internal redirection cycle while internally redirecting to “/index.php
Here’s why. When NGINX receives request for /some.php
, it takes us to our location ~ \.php$ {.
From there it checks the file for existence. And since it doesn’t have any access to it whatsoever, it will rewrite it to /index.php
in order to handle 404, then go in the same location ~ \.php$ {.
, and check index.php
for existence, and get stuck in a loop.
All simply because it has no access for .php files.
Come on, just let NGINX read the .php files. It’s OK. Just let it be.
Well, I don’t want to if I can. There is never enough security. And secure chmod is something essential. Let’s keep trying.
So how about …
location ~ \.php$ {
try_files $uri =404;
...
fastcgi_pass unix:/path/to/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
That’s cuter. No if
s, just try_files
. All as per NGINX ninja style. But wait, how about having the error page displayed by your PHP framework? Doable also with:
location ~ \.php$ {
try_files $uri /index.php =404;
...
fastcgi_pass unix:/path/to/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
Stop right there. We’re still letting NGINX check file for existence, so it’s still going to fail if it has no access to the scripts in our secure setup.
So what to do?
Best solution
location = /404.php {
fastcgi_intercept_errors off;
fastcgi_pass unix:/path/to/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
...
}
location ~ \.php$ {
error_page 404 /404.php;
fastcgi_intercept_errors on;
fastcgi_pass unix:/path/to/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
...
}
I owe you some explanation, right? In our location ~ \.php$ {
we tell NGINX, that when it sees PHP-FPM giving it a 404 HTTP status in the response, it can handle it on its own way. That is, discarding the HTML we get from PHP-FPM and using something else. But what?
We tell NGINX what page to display upon receiving 404 from PHP-FPM, here: error_page 404 /404.php;
.
We have to set up a dedicated location for /404.php
because we know this script exists (you have to create it, of course, if your framework has none; and it must be different from the front page handler like index.php
). We don’t want to discard PHP-FPM output for it, so we put fastcgi_intercept_errors off;
. Which is the default, so can be omitted but kept for illustration of what happens there.
Now NGINX doesn’t need to check for script existence. PHP-FPM will be the one to tell it.
So there you have it: a clean, tidy solution to 404 pages on missing .php script files in NGINX. It’s also secure and allows for lockdown chmod.