Setting Up PHP-FPM with nginx
nginx does not run PHP itself. Unlike Apache with mod_php, it hands PHP requests to a separate FastCGI process manager, PHP-FPM, over a socket, and serves the result. This split is why an nginx box handles static files so cheaply, but it also means there is one more service to configure and one more place things can go wrong. The classic symptom is a browser downloading index.php as a file instead of running it.
I've set this up more times than I can count, and every time I end up on the same handful of pages. This is my version for Ubuntu 16.04 with the stock PHP 7.0 packages, plus the pool tuning that keeps a small server from running out of memory. It assumes nginx and a working server block already exist.
Install PHP-FPM and extensions
sudo apt update
sudo apt install php7.0-fpm php7.0-mysql php7.0-mbstring php7.0-xml php7.0-curl php7.0-gd php7.0-zip
Do not install the php7.0 metapackage on an nginx server; it pulls in libapache2-mod-php7.0 and Apache along with it. Check the service came up:
systemctl status php7.0-fpm
If you need PHP 7.1, the ppa:ondrej/php PPA has it, and every path below just changes 7.0 to 7.1.
Find the socket
The pool configuration is /etc/php/7.0/fpm/pool.d/www.conf. The relevant line is:
listen = /run/php/php7.0-fpm.sock
A Unix socket is the right choice when nginx and PHP-FPM are on the same machine. If you ever move PHP to another host, change this to listen = 127.0.0.1:9000 and set listen.allowed_clients accordingly. Also check that the socket is owned by the user nginx runs as (www-data on Ubuntu):
listen.owner = www-data
listen.group = www-data
These are the defaults on Ubuntu, so normally nothing to change here.
Point nginx at it
Edit the server block, for example /etc/nginx/sites-available/example.com, and add index.php to the index list plus a location for PHP files:
server {
listen 80;
server_name example.com;
root /var/www/example.com/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
The snippets/fastcgi-php.conf file that ships with Ubuntu's nginx package sets SCRIPT_FILENAME, splits the path info, and includes fastcgi_params. It also contains try_files $fastcgi_script_name =404;, which stops nginx from passing requests for non-existent .php files to PHP-FPM, a common security hole in hand-written configs.
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Sizing the pool
The default pool uses pm = dynamic with pm.max_children = 5. Whether that's right depends on how much memory each PHP worker uses. Don't copy someone else's numbers (mine included); measure under load:
ps -ylC php-fpm7.0 --sort:rss
Take the typical RSS in KB, decide how much RAM you can give PHP, and divide. On a 2 GB VPS where each worker is around 40 MB and MySQL needs its share, I end up with something like:
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
pm.max_requests recycles workers after a number of requests, which contains slow memory leaks in older extensions. Restart (not reload) PHP-FPM after changing pm.* values:
sudo systemctl restart php7.0-fpm
php.ini
The FPM copy is /etc/php/7.0/fpm/php.ini (the CLI has its own, which I have edited by mistake before). The settings I always change:
cgi.fix_pathinfo=0
expose_php = Off
upload_max_filesize = 32M
post_max_size = 32M
date.timezone = Asia/Seoul
cgi.fix_pathinfo=0 matters most: with it on, a request for /uploads/image.jpg/x.php can cause PHP to execute image.jpg as PHP if the snippet's try_files guard is ever removed. Restart PHP-FPM again after editing.
Quick test
Create a test file and fetch it:
echo '<?php phpinfo();' | sudo tee /var/www/example.com/html/info.php
curl -s http://example.com/info.php | grep -o 'PHP Version [0-9.]*' | head -1
You should get the version string back, and the full page in a browser shows Server API: FPM/FastCGI. Delete info.php when you are done; it leaks far too much about the server to leave lying around.
If it breaks
- Browser downloads the
.phpfile: thelocation ~ \.php$block is missing or not matching, so nginx served it as a static file. Checknginx -Tfor the effective config. 502 Bad Gatewaywithconnect() to unix:/run/php/php7.0-fpm.sock failed (2: No such file or directory)in/var/log/nginx/error.log: PHP-FPM is not running, or thelistenpath inwww.confdoes not matchfastcgi_pass.502 Bad Gatewaywith(13: Permission denied): the socket owner or mode does not match nginx's user. Fixlisten.owner/listen.groupin the pool config.504 Gateway Time-out: the script ran longer thanfastcgi_read_timeout(default 60s). Either fix the script or raise the timeout in the PHP location block for that specific site./var/log/php7.0-fpm.logshowsserver reached pm.max_children setting (5), consider raising it: that's the pool sizing section above.
After that, the only thing I touch is pm.max_children when traffic changes.