Using Nginx as a caching proxy in front of WordPress

When I first started monkeying around with Nginx about a year ago, I approached it as a typical Apache fanboy. I’ve used Apache for 10+ years and it’s been a champ most of the time. Anyone familiar with Apache knows that you can do just about anything with it. It’s really the Swiss-army knife of webservers.

So it was with some skepticism that I installed nginx and started playing around with it. The first thing that struck me was the simplicity of the configuration. The second was the speed. I read this story on Ars Technica which detailed one guy’s experience replacing Apache with Nginx for serving zillions of static files – something at which Nginx excels. The third thing that struck me about Nginx was the philosophy of the project. From “Why Use It”

Apache is like Microsoft Word, it has a million options but you only need six. Nginx does those six things, and it does five of them 50 times faster than Apache.

— Chris Lea

So, to put in less pejorative terms, Apache is the Swiss-army knife while Nginx is an Xacto knife.

I quickly became a fan of Nginx, but I wasn’t going to dump Apache. Apache’s still great, and sometimes it’s just not worth the effort to gut a site and rebuild it with Apache. But after some more reading about Nginx features and use-cases, I started to realize I didn’t necessarily drop Apache to leverage some of Nginx’s awesomeness.

One of the most common uses for Nginx is as a reverse-proxy, sometimes as a load-balancer. But Nginx also makes it really easy to use it as a caching reverse-proxy. This is commonly done for caching static content in front of another webserver (sometimes Apache), letting you leverage the awesome performance of Nginx without losing any of Apache’s functionality. This is what I really wanted to do… I had a couple of Apache-based sites that were getting swamped, in some cases apparently due to the volume of image requests – prime candidate for reverse-proxying. Another was a WordPress blog on a severely underpowered server.

The config for caching reverse-proxy is really trivial; there are examples all over the internet, but here’s the gist:

proxy_cache_path  /var/lib/nginx/cache/staticfiles  levels=1:2   keys_zone=staticfilecache:60m inactive=90m  max_size=50m;
proxy_temp_path /var/lib/nginx/proxy;
proxy_connect_timeout 30;
proxy_read_timeout 120;
proxy_send_timeout 120;

upstream blah {
        server  127.0.0.1:88;
}

server {
 
        location ~* /evan/.+.(jpg|png|gif|jpeg|css|js|mp3|wav|swf|mov|ico)$ {
                        proxy_cache_valid 200 120m;
                        expires 864000;
                        proxy_pass http://blah;
                        proxy_cache staticfilecache;
        }
}

This instructs Nginx to cache static content (images, js, audio, video) requests that result in a 200 OK from the upstream for 120 minutes. The proxy_cache_path directive describes the “caching area,” specifying the on-disk path for cached objects, how many objects to cache (60m), the maximum size of the cache (50 MB), and after how much inactivity to delete them (90 minutes). That’s a whole lot of awesome in a couple of config lines.

So, this all works, but I wanted to cache the actual WordPress content pages as well, since each page requires hitting the MySQL DB, and most of the sites I manage are rarely updated. I created another key zone (caching area) for PHP content and told it to cache for 20 minutes. There were some problems with this though:

  1. I didn’t want to cache (or serve cached versions of) the WordPress admin pages. It’s important to distinguish between caching (the temporary storage on the proxy of the content generated by the source server) and serving of cached content, since Nginx lets you specify settings for both actions independently. The easiest workaround I found for this was putting a .htaccess file in the wp-admin directory with the following:
    SSLRequireSSL
    Header set Cache-Control "no-cache"
    Header set X-Accel-Expires "0"
    Header set Expires "Wed, 1 Jun 2011 20:00:00 GMT"
    

    Any of these headers should be enough to force Nginx not to cache the contents; I have them all in there just to be safe. SSLRequireSSL forces https for admin.

  2. I didn’t want to cache (or serve cached versions of) any page while I was logged in. When logged in, there’s a black bar at the top of the page with links to “edit post” or “go to dashboard,” and I wasn’t it them in some cases (I was being shown the non-logged-in cached page), and in some cases it would cache the logged-in version of the page, showing it to non-logged-in people, both bad situations. Based on this example, I added this to my Nginx config:
    map $http_cookie $logged_in {
        default 0;
        ~wordpress_logged_in 1; # WordPress session cookie
    }
    server {
    
            location / {
                    proxy_pass http://blah/;
                    proxy_redirect  http://blah/  http://$host/;
    
                    proxy_cache_bypass $logged_in;
                    proxy_no_cache $logged_in;
    
                    proxy_cache php;
                    proxy_cache_valid 200 30m;
                    expires 5m;
            }
    }
    

    If logged in, both proxy_cache_bypass and proxy_no_cache are set to “1”, so all my logged-in request bypass the cache entirely.

  3. I have the WPTouch plugin installed so iPhone users see a more iPhone-friendly version (rather than the regular site shrunk down to the iPhone’s screen size. I didn’t want desktop users seeing cached iPhone versions of the pages, and vice versa. To solve this, I set a $mobile var if “iphone” or “android” appear in $http_user_agent and incorporate that value into the proxy_cache_key. This way every page is still cached, but mobile users see the mobile version, and desktop users see the desktop version:
            set $mobile "_not_mobile_";
            if ($http_user_agent ~* "iPhone") {
                    set $mobile "mobile";
            }
            if ($http_user_agent ~* "Android") {
                    set $mobile "mobile";
            }
            location / {
                    proxy_pass http://blah/;
                    proxy_redirect  http://blah/  http://$host/;
    
                    proxy_cache_key "$mobile.$scheme$host$request_uri";
    
                    proxy_cache_bypass $logged_in;
                    proxy_no_cache $logged_in;
    
                    proxy_cache php;
                    proxy_cache_valid 200 30m;
                    expires 5m;
            }
    
    

So far this config has been working out pretty well. Uncached pages still load slow, but subsequent cached loads are pretty quick. I tweak it regularly, but here’s what I have currently, with Apache listening on 8888:

# WordPress reverse-proxy config
proxy_cache_path  /var/lib/nginx/cache/staticfiles  levels=1:2   keys_zone=staticfilecache:60m inactive=90m  max_size=50m;
proxy_cache_path  /var/lib/nginx/cache/php levels=2:2 keys_zone=php:30m inactive=60m max_size=50m;
proxy_temp_path /var/lib/nginx/proxy;
proxy_connect_timeout 30;
proxy_read_timeout 120;
proxy_send_timeout 120;
 
proxy_cache_key "$scheme$host$request_uri";
# http://wp-performance.com/2010/10/nginx-reverse-proxy-cache-wordpress-apache/

map $http_cookie $logged_in {
    default 0;
    ~wordpress_logged_in 1; # WordPress session cookie
}

upstream apache {
        server  127.0.0.1:8888;
}

server {
        proxy_cache_valid 200 20m;
 
        listen 80 default_server;
        server_name _;
 
        access_log  /var/log/nginx/combined-access.log combined;

        proxy_set_header X-Real-IP  $remote_addr;
 
        proxy_set_header Host $host;
 
        proxy_set_header X-Forwarded-For $remote_addr;

        proxy_set_header X-NginX-Proxy true;

        set $mobile "_not_mobile_";
        if ($http_user_agent ~* "iPhone") {
                set $mobile "iphone";
        }
        if ($http_user_agent ~* "Android") {
                set $mobile "android";
        }

        location / {
                proxy_pass http://apache/;
                proxy_redirect  http://apache/  http://$host/;
        }
 
        location /evan/ {
                proxy_pass http://apache/evan/;
                proxy_redirect  http://apache/  http://$host/;

                proxy_cache_key "$mobile.$scheme$host$request_uri";

                proxy_cache_bypass $logged_in;
                proxy_no_cache $logged_in;

                proxy_cache php;
                proxy_cache_valid 200 30m;
                expires 5m;
        }

        location /evan/wp-admin/ {
                proxy_pass http://apache/evan/wp-admin/;
                proxy_redirect  http://apache/  http://$host/;
        }

        location ~* /evan/.+.(jpg|png|gif|jpeg|css|js|mp3|wav|swf|mov|ico)$ {
                        proxy_cache_valid 200 120m;
                        expires 864000;
                        proxy_pass http://apache;
                        proxy_cache staticfilecache;
        }
 
        location = /50x.html {
                root   /var/www/nginx-default;
        }
 
        # No access to .htaccess files.
        location ~ /.ht {
                deny  all;
        }
 
        }

One Reply to “Using Nginx as a caching proxy in front of WordPress”

  1. Great post! Pretty much exactly my experience. I deployed WordPress once on nginx+fast_cgi+php-fpm and even though it was fast, it broke for reasons that were nearly impossible to troubleshoot. I have more mature versions of nginx, fast_cgi and php-fpm to work with now, but where I have hardly any user accounts (regional news website) I’d rather stick with the proxy caching model.

    My only question is, how has this worked for you since March? Are you hitting limits using apache? I plan on using this post as a template to build some dedicated load-balanced servers in about a week from now.

    Thanks!

Comments are closed.

%d bloggers like this: