Nginx, PHP-FPM caching done right
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.