How to Configure nginx Reverse Proxy WordPress Cache for Apache

Apache is not known for its speed. On the contrary, Apache has garnered a reputation for being rather bloated and performing well under high traffic. However, Apache is still the most popular web server around the world and is used by many hosting companies due to its familiarity and htaccess. If you still love Apache for some reason and want to speed up your WordPress site you can put an nginx reverse proxy caching solution in front of Apache to give your users a faster experience.

nginx reverse proxy cache works by sitting in front of Apache. nginx listens on port 80 and Apache listens on port 8080. nginx will serve any content it can cache while all other requests are sent to Apache for PHP processing with MySQL or MariaDB.

Note: This guide will not work ideally for WooCommerce, a new guide may be published for an nginx proxy cache that works for WooCommerce. If you want me to make you one contact me.

nginx-reverse-proxy-cache-apache-wordpress

Configure nginx Reverse Proxy WordPress Cache for Apache

Installation Overview

How to Configure Apache for nginx Reverse Proxy

Open Apache ports file

sudo nano /etc/apache2/ports.conf

Change port to 8080

Listen 8080

Open your Apache virtual host

sudo nano /etc/apache2/sites-available/wordpress.conf

Change Virtualhost port to 8080

<VirtualHost *:8080>

Ctrl+X, Y and Enter to save

You will need to change all of your Apache virtual hosts to listen on port 8080.

Apache will be restarted after nginx is installed and configured to avoid any downtime.

Install nginx

Install nginx and the nginx-extras package to get the ngx_cache_purge module which will make it easier to manage then nginx proxy cache.

sudo apt-get install nginx nginx-extras -y

Create nginx configuration

sudo nano /etc/nginx/sites-available/reverse

Paste the nginx configuration, we need the proxy_buffer values at the top to prevent this error (source)

upstream sent too big header while reading response header from upstream errors with buffers

Here is the actual nginx proxy cache configuration, note that this is not optimized for WooCommerce.

Remember to change Web.Server.IP with your server’s IP address.

# WP Bullet nginx proxy cache
# Author Mike from https://guides.wp-bullet.com
#fix 504 gateway timeouts, can go in nginx.conf
proxy_connect_timeout       600;
proxy_send_timeout          600;
proxy_read_timeout          600;
send_timeout                600;
#set the location of the cached files, zone, name, size (1000 MB) and how long to cache for 600 minutes
proxy_cache_path  /var/run/proxy_cache levels=1:2 keys_zone=WORDPRESS-PROXY:10m max_size=1000m inactive=600m use_temp_path=off;
proxy_cache_key $scheme$host$request_uri;
#prevent header too large errors
proxy_buffers 256 16k;
proxy_buffer_size 32k;
#httpoxy exploit protection
proxy_set_header Proxy "";
# add forwarded for header
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

server {
listen          80 default;
access_log /var/log/nginx/proxy-access.log;
error_log /var/log/nginx/proxy-error.log;
# show cache status and any skip cache reason
add_header WP-Bullet-Proxy-Cache $upstream_cache_status;
add_header Cache-BYPASS-Reason $skip_reason;

# define nginx variables
set $do_not_cache 0;
set $skip_reason "";
set $bypass 0;

# security for bypass so localhost can empty cache
if ($remote_addr ~ "^(127.0.0.1|Web.Server.IP)$") {
    set $bypass $http_secret_header;
}

# skip caching WordPress cookies
if ($http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
    set $do_not_cache 1;
    set $skip_reason Cookie; 
}

# Don't cache URIs containing the following segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|sitemap(_index)?.xml") {
    set $skip_cache 1;
    set $skip_reason URI; 
}

location / {
	proxy_set_header Host $host;
    # may need to comment out proxy_redirect if get login redirect loop
    proxy_redirect off;
    proxy_cache WORDPRESS-PROXY;
    proxy_cache_revalidate on;
    proxy_ignore_headers  Expires Cache-Control;
    proxy_cache_use_stale  error timeout invalid_header updating http_500 http_502 http_503 http_504;
    proxy_cache_bypass $bypass $do_not_cache;
    proxy_no_cache $do_not_cache;
    proxy_cache_valid 200 301 302 500m;
    proxy_cache_valid 404 1m;
    #can rename PURGE to whatever you want, should restrict it to backend server requests for security
    proxy_cache_purge PURGE from 127.0.0.1 Web.Server.IP;
    # pass requests onto your PHP backend
    proxy_pass  http://127.0.0.1:8080;
    }

# allows purging via special URL
location ~ /purge(/.*) {
    allow 127.0.0.1;
    allow Web.Server.IP;
    deny all;
    proxy_cache_purge WORDPRESS-PROXY $scheme$host$1;
    }
}

Ctrl+X, Y and Enter

Symlink the nginx reverse proxy cache for WordPress virtual host so it will be enabled when we restart nginx

sudo ln -s /etc/nginx/sites-available/reverse /etc/nginx/sites-enabled/reverse

Unlink the default nginx virtual host

unlink /etc/nginx/sites-enabled/default

Restart Apache and nginx

sudo service apache2 restart
sudo service nginx restart

Testing the nginx Reverse Proxy Cache

We can use cURL to test that the nginx reverse proxy is caching our WordPress site

sudo apt-get install curl -y

Now cURL is installed we can start testing the nginx reverse proxy in front of Apache

Use SSH on the Web server to run these cURL commands. This will test if your homepage is cached by the reverse proxy, the -I flag ensures we get the response headers back from the reverse proxy server

curl -I https://wp-bullet.com/

The key value here is the WP-Bullet-Proxy-Cache status

HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 17:32:24 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
WP-Bullet-Proxy-Cache: HIT

If the home page isn’t cached you will get a MISS in the WP-Bullet-Proxy-Cache response

HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 17:35:53 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
WP-Bullet-Proxy-Cache: MISS

Sometimes you may need to cURL the same URL twice to get a HIT response.

How nginx Stores Cache

If you look in the proxy_cache_path folder you will see a bunch of seemingly random letters and numbers which are decided by the levels=1:2. This can seem confusing initially since nginx stores cache as md5 hashes of the URLs based on the proxy_cache_key. we are using $scheme$host$uri here

  • $scheme=http
  • $host=domain
  • $request_uri=URL

So for this page https://guides.wp-bullet.com/about

  • $scheme is https
  • $host is guides.wp-bullet.com
  • $request_uri is /about

We can pass this through an md5 generator on Debian

echo https://wp-bullet.com/about | md5sum

It generates this md5 sum

c301d2e9d39fa7434a56322a09dbab17

which nginx uses to create the folder structure based on the proxy_cache_path levels=1:2.

Here the 1 in 1:2 refers to the 7, the character all the way to the right at the end of the original md5 hash.

The 2 in 1:2 refers to the b1 which are the characters to the left of the top level key 1 (here 7).

c301d2e9d39fa7434a56322a09dbab17

From levels=1:2, the 1 (the 7) becomes the top level folder and the 2 (b1) becomes its sub directory, with the original md5 hash as the filename

/var/run/proxy-cache/7/b1/c301d2e9d39fa7434a56322a09dbab17

Knowing how nginx cache works means we can selectively deleted items from the reverse proxy cache.

Purging and Invalidating the nginx Reverse Proxy cache

Thanks to the ngx_cache_purge module which is included in the nginx-extras module we have several ways to invalidate cache selectively. Our goal is to have the entire site cached using this plugin so your WordPress site is completely cached and always fast. When we update content we only want to empty the cache for those posts, pages or categories which are changed and replace those old items with fresh new ones immediately so your users get the fastest possible experience.

The WP Bullet nginx cache plugin allows you to refresh the cache using all of these methods except the PURGE method.

  • nginx proxy cache is stored in a folder structure in the /var/run/proxy-cache folder which we can selectively delete specific items from or delete everything to empty the entire cache
  • BYPASS lets you force a refresh of the post or page by asking the Web server serving WordPress for a new version
    • The refreshed item will replace the old outdated item in the nginx reverse proxy cache
  • PURGE method, the proxy_cache_purge lets you use non-RFC HTTP methods to purge specific items from cache
  • /purge URL method lets you append a URL to the purge location to empty a specific item

WP Bullet nginx cache supports all of these methods, this is how to test if they are working using cURL.

Empty Entire nginx Reverse Proxy Cache

If you want to empty the entire cache you can simply delete the proxy-cache folder contents manually

rm -R /var/run/proxy-cache/*

You can also delete specific items if you want by creating an md5 hash of the full URL you want to purge and deleting the specific folder and subfolder recursively in the proxy_cache_path folder. The WP Bullet nginx cache plugin does this for you.

If you want to empty the entire cache using regular expressions (also known as wildcards) your only option is to use nginx Plus which costs money. The nginx plus crew know that having a high performance WordPress site means keeping your entire site cached all the time so large companies will pay for flexible cache control.

Refresh Items in nginx Reverse Proxy with BYPASS Method

Bypass is definitely the best way to invalidate and refresh your nginx reverse proxy cache. with proxy_cache_bypass you force the nginx reverse proxy to fetch a new version of the URL from the web server running WordPress and replace the old outdated version with the new fresh version. Your users will never get old content this way and will never get slow PHP compiled on-the-fly versions from your web server (unless they aren’t cacheable).

In the block above we implemented proxy_cache_bypass only for requests that come from our web server or the reverse proxy itself

We enabled the secret header for incoming requests from the web server and reverse proxy so we can test using the secret header with cURL from those servers.

curl -I https://wp-bullet.com -H "secret-header: true"

You will see this output showing BYPASS in the X-Cache header

HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 17:30:44 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
X-Cache: BYPASS

If you try the same cURL command from another server you will just see a HIT response

HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 17:33:23 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
X-Cache: HIT

The WP Bullet nginx cache plugin will run all of these commands automatically for you when you update your posts, pages and categories.

Refresh Items in nginx Reverse Proxy with PURGE Method

This PURGE method is courtesy of the ngx_cache_purge module found in the nginx-extras package

For this method to work I found the proxy_cache_key had to be set to $scheme$host$request_uri but your experience may vary

To submit a PURGE request use this syntax, the proxy_cache_purge module will translate the request into the md5 hash of the URL and delete the item from the proxy_cache_path folder specified in the nginx reverse proxy virtual host.

curl -X PURGE -I https://wp-bullet.com

If the reverse proxy has the file you will get a 200 response meaning it was successful

HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Tue, 29 Mar 2016 23:55:07 GMT
Content-Type: text/html
Content-Length: 277
Connection: keep-alive

If the nginx reverse proxy does not have that specific URL cached then you will get a 404

HTTP/1.1 404 Not Found
Server: nginx/1.8.1
Date: Tue, 29 Mar 2016 23:56:11 GMT
Content-Type: text/html
Content-Length: 168
Connection: keep-alive

If the nginx reverse proxy detects that your IP address is not allowed to execute PURGE requests you will get a 403 forbidden response

HTTP/1.1 403 Forbidden
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 17:13:45 GMT
Content-Type: text/html
Content-Length: 168
Connection: keep-alive

This type of nginx reverse proxy security is important because it limits PURGE requests by whitelisting your trusted servers

Purge Items in nginx Reverse Proxy with /purge URL Method

The /purge URL method uses a specific URL to call the nginx proxy_cache_purge method which we implemented above.

To purge via URL use this cURL comand to purge the homepage represented by the trailing slash

curl https://wp-bullet.com/purge/ -I

You will see this response if the home page was cached in the nginx reverse proxy and you successfully emptied it

HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 13:55:47 GMT
Content-Type: text/html
Content-Length: 277
Connection: keep-alive

If your nginx reverse proxy doesn’t have your WordPress home page cached you will see a 404 error

HTTP/1.1 404 Not Found
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 13:58:50 GMT
Content-Type: text/html
Content-Length: 168
Connection: keep-alive

If your nginx reverse proxy does not allow you to access the /purge location you will get a 403 forbidden error

HTTP/1.1 403 Forbidden
Server: nginx/1.8.1
Date: Wed, 30 Mar 2016 17:16:39 GMT
Content-Type: text/html
Content-Length: 168
Connection: keep-alive

Similar to the PURGE method we are limiting access to the /purge location by whitelisting IP addresses so attackers cannot overwhelm your web server running WordPress

Get the Real IP in Apache Logs from nginx

You will need to open your Apache virtualhost to adjust the logging format

sudo nano /etc/apache2/sites-available/wpbullet.conf

Find this line containing LogFormat %h

LogFormat "%h %l %u %t \"%r\" %>s %b" combined

Replace the %h with %{X-Forwarded-For}i to use the header we set in nginx.

LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b" combined

If you do not have a LogFormat line then add the whole line as you see it above within your block
Test your Apache virtual host syntax is OK

sudo apachectl configtest

If it’s all good then reload Apache

sudo service apache2 reload

Now the logs will show the correct IP.

Sources

Where does /var/lib/nginx/proxy come from?
Getting Real IP for Apache from nginx