Nginx, PHP-FPM caching done right · uwot.eu
another meaningless subtitle

Nginx, PHP-FPM caching done right

· by fabio · Read in about 5 min · (859 Words)
cache enfold mainwp nginx php php-fpm wordpress CentOS 7 FreeBSD

The whole web is full of pseudo guides on how to properly - that is the key word here - configure Nginx to perform caching alongside with PHP-FPM, but every single one of them fails to mention some minor steps resulting in a borked half functioning implementation.
For example, not a single one mention the necessity to edit /etc/php.ini and set session.use_cookies to 0.
Too bad that without doing so caching with WordPress in combination with certain plugins or themes (for example MainWP or Enfold theme) is completely not working; the following headers get added to every HTTP response:

Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=***; path=/

.:. PHP-FPM

So, let’s start editing php.ini:

[root@CentOS ~]$ vi /etc/php.ini 
---
session.use_cookies = 0
session.cache_limiter = public

The next thing to do is edit the pfp-fpm configuration files (pm.* settings tuning is based on installed RAM and CPU cores, also a chroot can be set):

[root@CentOS ~]$ vi /etc/php-fpm.conf
---
daemonize = yes
[root@CentOS ~]$ vi /etc/php-fpm.d/www.conf
---
listen = /var/run/php-fpm/php-fpm.sock
listen.allowed_clients = 127.0.0.1
user = nginx
group = nginx
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 3

.:. Nginx installation

When using CentOS I normally add Nginx’s yum repo so that I always have the latest stable version installed.
On FreeBSD things are even easier because the latest stable Nginx version is already in the repos.

[root@CentOS ~]$ yum install nginx
[root@FreeBSD ~]$ pgk install nginx

.:. Nginx configuration

The procedure here is basically the same on both OS.

[root@CentOS ~]$ vi /etc/nginx/nginx.conf
---

user nginx;
worker_processes 1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/v.hosts/*.conf;
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log  main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    include /etc/nginx/conf.d/*.conf;

    fastcgi_cache_path /dev/shm/nginx-cache/ levels=1:2 keys_zone=CACHE:100m max_size=200m inactive=1d;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    add_header Cache $upstream_cache_status;
}

The interesting bits here are fastcgi_cache_path, fastcgi_cache_key and add_header parameters; for more information look up the official Nginx documentation or search with DDG.
Certain parameters (worker_processes, worker_connections) can be tuned depending on the hardware configuration (number of CPU cores, amout of RAM, network bandwidth, etc).

[root@CentOS ~]$ vi /etc/nginx/v.hosts/domain.smt.conf
---
server {
    listen 80;
    server_name domain.tld;

    access_log      /var/log/nginx/access.log;
    error_log       /var/log/nginx/error.log;
    
    root /usr/share/nginx/html/domain.tld;
    
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types *;

    return 301 https://$host$request_uri;

    error_page   404 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

server {
    listen 443 ssl http2;
    ssl on;
    server_name domain.tld;
    
    root /usr/share/nginx/html/domain.tld;
    index index.php index.html index.htm;

    access_log      /var/log/nginx/ssl-access.log;
    error_log       /var/log/nginx/ssl-error.log;

    ssl_certificate         /etc/nginx/ssl/domain.tld/fullchain.pem;
    ssl_certificate_key     /etc/nginx/ssl/domain.tld/privkey.pem;

    ssl_protocols TLSv1.2;

    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';   
 
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/ssl/dhparams.pem;
    keepalive_timeout    60;
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  10m;

    # HTTP compression
    gzip on;
    gzip_static on;
    gzip_types
        text/plain
        text/css
        text/js
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        image/svg+xml;

    client_max_body_size 16m;
    
    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Block pages from loading when they detect reflected XSS attacks
    add_header X-XSS-Protection "1; mode=block";

    # Prevent browsers from incorrectly detecting non-scripts as scripts
    add_header X-Content-Type-Options "nosniff";

    # Allow only framer.mozilla.org to frame site
    # Note that this blocks framing from browsers that don't support CSP2+
    #add_header Content-Security-Policy "frame-ancestors https://framer.mozilla.org";
    add_header X-Frame-Options "DENY";

    # Set Referrer Policy header
    add_header Referrer-Policy "no-referrer";

    # Set Feature Policy header
    #add_header Feature-Policy "geolocation 'none'; microphone 'none'; camera 'none'; speaker 'none'; payment 'none'";
    
    #fastcgi_cache start
    set $skip_cache 0;

    # POST requests and urls with a query string should always go to PHP
    if ($request_method = POST) {
            set $skip_cache 1;
    }
    if ($query_string != "") {
            set $skip_cache 1;
    }

    # Don't cache uris containing the following segments
    if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
            set $skip_cache 1;
    }

    # Don't use the cache for logged in users or recent commenters
    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
            set $skip_cache 1;
    }
    
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    
    location ~ [^/]\.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        if (!-f $document_root$fastcgi_script_name) {
            return 404;
        }
        
        # This is a robust solution for path info security issue and works with "cgi.fix_pathinfo = 1" in /etc/php.ini (default)
        # Zero-day exploit defense.
        # http://forum.nginx.org/read.php?2,88845,page=3
        # Won't work properly (404 error) if the file is not stored on this server, which is entirely possible with php-fpm/php-fcgi.
        # Comment the 'try_files' line out if you set up php-fpm/php-fcgi on another machine.  And then cross your fingers that you won't get hacked.
        try_files $uri =404;

        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;
        include fastcgi_params;

        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;

        fastcgi_cache CACHE;
        fastcgi_cache_valid 200 1d;
    }

    error_page   404 500 502 503 504 /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Configuration here is pretty much straightforward once it is written, basically: cache everything but admin pages.
This way the WordPress site is transformed in a static site, the performance improvement is huge and the load on the server is also decreased by some order of magnitudes.
One last thing to do is configure WordPress to perform some intelligent cache purging operations whenever someone posts a new comment or a new page is added, this can be done easily by installing a plugin like Nginx Helper or Nginx Cache.