DB-유

Setting Up PHP-FPM with nginx

· linux, nginx, php

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 .php file: the location ~ \.php$ block is missing or not matching, so nginx served it as a static file. Check nginx -T for the effective config.
  • 502 Bad Gateway with connect() 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 the listen path in www.conf does not match fastcgi_pass.
  • 502 Bad Gateway with (13: Permission denied): the socket owner or mode does not match nginx's user. Fix listen.owner/listen.group in the pool config.
  • 504 Gateway Time-out: the script ran longer than fastcgi_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.log shows server 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.