Tutorial

How to Setup FastCGI Caching with Nginx on your VPS

Published on October 29, 2013
How to Setup FastCGI Caching with Nginx on your VPS

Prelude


Nginx includes a FastCGI module which has directives for caching dynamic content that are served from the PHP backend. Setting this up removes the need for additional page caching solutions like reverse proxies (think Varnish) or application specific plugins. Content can also be excluded from caching based on the request method, URL, cookies, or any other server variable.

Enabling FastCGI Caching on your VPS


This article assumes that you’ve already setup and configured Nginx with PHP on your droplet. Edit the Virtual Host configuration file for which caching has to be enabled.

nano /etc/nginx/sites-enabled/vhost

Add the following lines to the top of the file outside the server { } directive:

fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=MYAPP:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

The “fastcgi_cache_path” directive specifies the location of the cache (/etc/nginx/cache), its size (100m), memory zone name (MYAPP), the subdirectory levels, and the inactive` timer.

The location can be anywhere on the hard disk; however, the size must be less than your droplet’s RAM + Swap or else you’ll receive an error that reads “Cannot allocate memory.” We will look at the “levels” option in the purging section-- if a cache isn’t accessed for a particular amount of time specified by the “inactive” option (60 minutes here), then Nginx removes it.

The “fastcgi_cache_key” directive specifies how the the cache filenames will be hashed. Nginx encrypts an accessed file with MD5 based on this directive.

Next, move the location directive that passes PHP requests to php5-fpm. Inside “location ~ .php$ { }” add the following lines.

fastcgi_cache MYAPP;
fastcgi_cache_valid 200 60m;

The “fastcgi_cache” directive references to the memory zone name which we specified in the “fastcgi_cache_path” directive and stores the cache in this area.

By default Nginx stores the cached objects for a duration specified by any of these headers: X-Accel-Expires/Expires/Cache-Control.

The “fastcgi_cache_valid” directive is used to specify the default cache lifetime if these headers are missing. In the statement we entered above, only responses with a status code of 200 is cached. Other response codes can also be specified.

Do a configuration test

service nginx configtest

Reload Nginx if everything is OK

service nginx reload

The complete vhost file will look like this:

fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=MYAPP:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

server {
    listen   80;
    
	root /usr/share/nginx/html;
	index index.php index.html index.htm;

	server_name example.com;

	location / {
	    try_files $uri $uri/ /index.html;
    }

	location ~ \.php$ {
	    try_files $uri =404;
	    fastcgi_pass unix:/var/run/php5-fpm.sock;
	    fastcgi_index index.php;
	    include fastcgi_params;
	    fastcgi_cache MYAPP;
	    fastcgi_cache_valid 200 60m;
    }
}

Next we will do a test to see if caching works.

Testing FastCGI Caching on your VPS


Create a PHP file which outputs a UNIX timestamp.

 /usr/share/nginx/html/time.php

Insert

<?php
echo time();
?>

Request this file multiple times using curl or your web browser.

root@droplet:~# curl http://localhost/time.php;echo
1382986152
root@droplet:~# curl http://localhost/time.php;echo
1382986152
root@droplet:~# curl http://localhost/time.php;echo
1382986152

If caching works properly, you should see the same timestamp on all requests as the response is cached. </br></br>

Do a recursive listing of the cache location to find the cache of this request.

root@droplet:~# ls -lR /etc/nginx/cache/
/etc/nginx/cache/:
total 0
drwx------ 3 www-data www-data 60 Oct 28 18:53 e

/etc/nginx/cache/e:
total 0
drwx------ 2 www-data www-data 60 Oct 28 18:53 18

/etc/nginx/cache/e/18:
total 4
-rw------- 1 www-data www-data 117 Oct 28 18:53 b777c8adab3ec92cd43756226caf618e

The naming convention will be explained in the purging section.

We can also make Nginx add a “X-Cache” header to the response, indicating if the cache was missed or hit.

Add the following above the server { } directive:

add_header X-Cache $upstream_cache_status;

Reload the Nginx service and do a verbose request with curl to see the new header.

root@droplet:~# curl -v http://localhost/time.php
* About to connect() to localhost port 80 (#0)
*   Trying 127.0.0.1...
* connected
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /time.php HTTP/1.1
> User-Agent: curl/7.26.0
> Host: localhost
> Accept: */*
>
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 200 OK
< Server: nginx
< Date: Tue, 29 Oct 2013 11:24:04 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Cache: HIT
<
* Connection #0 to host localhost left intact
1383045828* Closing connection #0

Setting Cache Exceptions


Some dynamic content such as authentication required pages shouldn’t be cached. Such content can be excluded from being cached based on server variables like “request_uri,” “request_method,” and “http_cookie.”

Here is a sample configuration which must be used in the server{ } context.

#Cache everything by default
set $no_cache 0;

#Don't cache POST requests
if ($request_method = POST)
{
    set $no_cache 1;
}

#Don't cache if the URL contains a query string
if ($query_string != "")
{
    set $no_cache 1;
}

#Don't cache the following URLs
if ($request_uri ~* "/(administrator/|login.php)")
{
    set $no_cache 1;
}

#Don't cache if there is a cookie called PHPSESSID
if ($http_cookie = "PHPSESSID")
{
    set $no_cache 1;
}

To apply the “$no_cache” variable to the appropriate directives, place the following lines inside location ~ .php$ { }

fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;

The “fasctcgi_cache_bypass” directive ignores existing cache for requests related to the conditions set by us previously. The “fastcgi_no_cache” directive doesn’t cache the request at all if the specified conditions are met.

Purging the Cache


The naming convention of the cache is based on the variables we set for the “fastcgi_cache_key” directive.

fastcgi_cache_key "$scheme$request_method$host$request_uri";

According to these variables, when we requested “http://localhost/time.php” the following would’ve been the actual values:

fastcgi_cache_key "httpGETlocalhost/time.php";

Passing this string through MD5 hashing would output the following string:

b777c8adab3ec92cd43756226caf618e

This will form the filename of the cache as for the subdirectories we entered “levels=1:2.” Therefore, the first level of the directory will be named with 1 character from the last of this MD5 string which is e; the second level will have the last 2 characters after the first level i.e. 18. Hence, the entire directory structure of this cache is as follows:

/etc/nginx/cache/e/18/b777c8adab3ec92cd43756226caf618e

Based on this cache naming format you can develop a purging script in your favorite language. For this tutorial, I’ll provide a simple PHP script which purges the cache of a __POST__ed URL.

/usr/share/nginx/html/purge.php

Insert

<?php
$cache_path = '/etc/nginx/cache/';
$url = parse_url($_POST['url']);
if(!$url)
{
    echo 'Invalid URL entered';
    die();
}
$scheme = $url['scheme'];
$host = $url['host'];
$requesturi = $url['path'];
$hash = md5($scheme.'GET'.$host.$requesturi);
var_dump(unlink($cache_path . substr($hash, -1) . '/' . substr($hash,-3,2) . '/' . $hash));
?>

Send a POST request to this file with the URL to purge.

curl -d 'url=http://www.example.com/time.php' http://localhost/purge.php

The script will output true or false based on whether the cache was purged to not. Make sure to exclude this script from being cached and also restrict access.

Submitted by: <a rel=“author” href=“http://jesin.tk/”>Jesin A</a>

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Jesin A

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
10 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

@jesin @kamaln7

i just changed a bit for my app. i used /var/run/cache location instead of the one stated on the tutorial. now i have 2 questions.

  1. does that location get mounted to RAM automatically or i need to mount that manually?
  2. what should be the folder permission? i already added that folder to ww-data:www-data but i checked the folder write permission is set to 700(which is default i think)

by the way, excellent tutorial. thank you.

#Don't cache if there is a cookie called PHPSESSID
if ($http_cookie = "PHPSESSID")

This is wrong. This check $http_cookie = "PHPSESSID" will only be true if the Cookie header exactly matches “PHPSESSID”. The Cookie header has the syntax cookie1_key=cookie1_val; cookie2_key=cookie2_val; ....

That means this condition will only be met if PHPSESSID is the only cookie in the request, and only if it doesn’t have any value (since this isn’t how PHP sessions work, this will never happen).

Instead, the test should be either

if ($http_phpsessid)

or

if ($http_cookie ~ "PHPSESSID")

(the first one is probably better in terms of performance)

Thanks for this tutorial!

Is there a reason why I can’t leave fastcgi_cache_path and fastcgi_cache_key in the http context, and all the rest just put in a snippet which I include in my location?

e.g something like

/etc/nginx/snippets/my-custom-cache.conf:

set $no_cache 0;

if ($request_uri = "/") {
    set $no_cache 1;
}
if ($query_string != "") {
    set $no_cache 1;
}
if ($http_cookie = "PHPSESSID") {
    set $no_cache 1;
}

include fastcgi_params;

fastcgi_cache mycustomcache;
fastcgi_cache_valid 200 1w;
fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;

add_header X-Origin-Cache $upstream_cache_status;

When I run this command

“nano /etc/nginx/sites-enabled/vhost”

in putty the file open up for editing but the file is empty so I cannot find anything such as

server { } directive

or

location ~ .php$ { }

What do I do?

Is there an update for this? This article is pretty old and references PHP 5. What about setting up with PHP 7?

One does not store caches in /etc. That’s what /var/cache is for.

Make sure to exclude this script from being cached and also restrict access. How should I exclude script from being cached please?

Hey, nice tutorials !

I got question about the PHP purging script. It’s the only snippet on the web that recreate from the URL the path and filename to the fastchi good fastcgi cache file to delete/unlink it (so not all cache delete, but selective one).

All other methods are using the typical Nginx server block with the nginx_cache_purge directive on a /purge/ style location.

But after few test (deleting cache, refreshing 1 page to get the cache generated), i copy/paste the url in your script (var_dump php sandbox), and the path/filename is wrong…

For a given URL, your script output "/etc/nginx/cache/7/53/f36240401c184bbf86422a360828a537" But the cache file is "/etc/nginx/cache/1/e5/933a3aae15aa23ce51a2a01b83e6fe51" (i double checked that the cache file was the one of the url opening it to view content).

So the matching is not correct, and unlink will fail. Where did you find the way to build the path/filename located in the $hash variable ?

Thanks a lot :)

This comment has been deleted

    Hello,

    I followed your instruction and I have been able to purge cache without any problem. Thanks for the great tutorial.

    However I had to modify little bit based on the following tutorial because of woocommerce cookie issues. https://easyengine.io/tutorials/wordpress/woocommerce-window-shopping-caching-technique/

    Since then, purge.php doesn’t work. I suspect it’s because I had added rt_session at the end of fastcgi_cache_key, but I’m not sure how to modify purge.php. Can you help me resolve this?

    Thanks!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.