Caching WordPress with Nginx

  • wordpress
  • caching

I'm running a WordPress website on a small VPS with the help of Nginx and PHP-FPM. But I noticed the site was not that fast as I wanted it to be. This is mainly caused by the little resources this VPS has. So every request needs to be as efficient as possible. To make that possible, I want to cache the requests with Nginx to minimize the requests that needs to be handled by PHP-FPM.

Configuration

I've created the following configuration.

# The path to store the cache files, limit the folder to 100MB
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m;

# A unique request is defined by this cache key
fastcgi_cache_key "$scheme$request_method$host$request_uri";

# Show the cached version if upstream gives a timeout or a HTTP 500 error
fastcgi_cache_use_stale error timeout invalid_header http_500;

# Don't use the following headers to define the cache variables
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

caching.conf hosted on GitHub Gist

upstream php {
    server 127.0.0.1:9000;
}
server {
    listen [::]:80;
    listen 80;
    server_name www.website.nl
    root /var/www/www.website.nl;
    index index.php;

    # Keepalive for 70 seconds
    keepalive_timeout 70;

    # Increase proxy buffers for large requests
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;

    # Upload limit
    client_max_body_size 50m;
    client_body_buffer_size 128k;

    # Initialize the variable that specified to skip the cache
    set $skip_cache 0;

    # POST requests and url's with a query string should always skip cache
    if ($request_method = POST) {
        set $skip_cache 1;
    }
    if ($query_string != "") {
        set $skip_cache 1;
    }

    # Don't cache url's containing the following segments
    if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.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;
    }

    # PHP files should go to PHP-FPM
    location ~ \.php$ {
        include fastcgi_params;

        # Extra security measure, but only possible when PHP-FPM is on the same server as Nginx
        try_files $uri =404;
        
        # Fastcgi configuration
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        fastcgi_intercept_errors on;
        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;

        # Don't cache when $skip_cache is true
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;

        # Use the WORDPRESS zone
        fastcgi_cache WORDPRESS;
        
        # Cache everything for 1 week
        fastcgi_cache_valid 1w;

        # Pass request to PHP-FPM        
        fastcgi_pass php;
    }
    
    # Don't write to accesslog for these files
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }
    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Media files with one of these extensions should be cached by the browser
    location ~* \.(xml|ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|css|rss|atom|js|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
        expires max;
        log_not_found off;
    }

    # Deny access to .* files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
    
    # Add cache status header for easy debugging
    add_header X-Cache $upstream_cache_status;
}

wordpress-website.conf hosted on GitHub Gist

The caching.conf file must be placed in the conf.d folder. This defines the cache path for the WORDPRESS zone, the cache_key, and how Nginx should handle when upstream returns an error. The wordpress-website.conf must be placed in the sites-available folder and a symlink must exist to this file from the sites-enabled folder.

Caching strategy

The caching strategy is simple; Try to cache every call to PHP-FPM for one week. There are a few exceptions to this rule ( see the $skip_cache variable in wordpress-website.conf):

  1. POST requests
  2. Requests with a query string
  3. Requests to:
  • /wp-admin
  • /xmlrpc.php
  • /wp-*.php
  • /feed
  • index.php
  • sitemap*.xml
  1. Request with one of the following cookies:
  • comment_author
  • wordpress_*
  • wp-postpass
  • wordpress_no_cache
  • wordpress_logged_in

These exceptions make sure you'll never get a cached page when you're logged in or just posted a comment.

Purging the cache

The downside of this simple but effective strategy is that there isn't a way to automatically purge the cache when you add or change something on your website. There are modules for Nginx to purge the cache and plugins for Wordpress to do this when needed. But, I don't need that. I'll just remove the cache manually:

$ sudo rm -rf /var/run/nginx-cache/*

See the result

Make sure you're not logged in to test how superfast your website is! Every page includes a X-Cache header which tells you whether a page is served from cache (hit) or not (miss).