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 author(s)

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
40 Comments
Leave a comment...

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!

Status: HTTP/1.1 200 OK Server: nginx/1.4.1 Date: Wed, 06 Nov 2013 02:55:23 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: close Vary: Accept-Encoding X-Powered-By: PHP/5.3.10-1ubuntu3.8 X-Pingback: http://mydomain/xmlrpc.php Link: http://mydomain/?p=2138; rel=shortlink rt-Fastcgi-Cache: HIT

Does previous lines mean the cache works? Why “Connection: close” always close?

Thanks!

Kamal Nasser
DigitalOcean Employee
DigitalOcean Employee badge
November 9, 2013

@handytutorial: Yes – <code>rt-Fastcgi-Cache: HIT</code> means that it was served from the cache.

this tutorial is very hard to follow and understand. My nginx couldn’t start at all.

@PigGy: Can you check the file at /var/log/nginx/error.log for any error and post them here.

@handytutorial: The Connection: close indicates the end of the HTTP connection.

From www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.10

HTTP/1.1 defines the “close” connection option for the sender to signal that the connection will be closed after completion of the response

@jesin.a: I destroyed the 2 droplets and recreated a fresh droplet to follow your tutorial again. this time i skipped the section /* droplet’s RAM + Swap */ because I not familiar with the settings and I want to save time to recreate a fresh droplet so to show you my error.log and other information as below.

/var/log/nginx/error.log: http://pastebin.com/nUXZxiKN

/var/www/site1/wordpress/purge.php: http://pastebin.com/EMxAWbP1

/etc/nginx/sites-available/wordpress: http://pastebin.com/rF885i2Y

Putty: curl -d ‘url=http://www.example.com/time.phphttp://localhost/purge.php http://pastebin.com/QMcV2xyx

Kamal Nasser
DigitalOcean Employee
DigitalOcean Employee badge
November 15, 2013

@PigGy: What version of nginx are you using?<pre>nginx -v</pre>

@Kamal Nasser My version: <pre>nginx version: nginx/1.4.3 </pre>

Kamal Nasser
DigitalOcean Employee
DigitalOcean Employee badge
November 18, 2013

@PigGy: Are you still experiencing this issue? It’s a bit odd, since $request_uri is valid and you’re not running a ridiculously old version of nginx so it should exist.

@Kamal Nasser I still facing the problem. I wouldn’t dare run Sudo Reboot because if I reboot my nginx webserver couldn’t start-up at all.

Kamal Nasser
DigitalOcean Employee
DigitalOcean Employee badge
November 22, 2013

@PigGy: Does it work if you just browse to / and not /purge.php? Try creating a file with <pre><?php phpinfo();</pre> as its contents – does it work?

@Kamal Nasser Do you have any email for me to send you my ip and pw for putty login?

@PigGy I’m the one who wrote this article you can get in touch with me using the “contact me” form of my blog http://jesin.tk/contact-me/

The “nocache” entries must be above the “location ~ .php$ {” line.

Also “/etc/nginx/cache” must be writable by the “www-data” user (chown it).

Sorry for missing out these points. You may contact me if you still face issues.

Also as far as I’ve seen WordPress installations must use the following “try_files”

try_files $uri $uri/ /index.php?$args;

Thanks for this tutorial. I’d like to add logging… I tried adding this in my vhost config (just after fastcgi_cache_path above server section): log_format custom '$remote_addr - $remote_user [$time_local] ’ '“$request” $status $body_bytes_sent ’ ‘“$http_referer” “$http_user_agent” nocache:$no_cache’; access_log /var/log/nginx/microcache.log custom;

I checked if the folder had the right permissions… and it had… microcache.log is always 0 bytes…

@multiformeingegno The <a href=“http://wiki.nginx.org/HttpLogModule#log_format”>log_format</a> directive should be placed only in the <code>http { }</code> directive.

To log cache statuses place the following in the <strong>nginx.conf</strong> file.

<pre>log_format custom '$remote_addr - $upstream_cache_status [$time_local] ’ '“$request” $status $body_bytes_sent ’ ‘“$http_referer” “$http_user_agent”’; access_log /var/log/nginx/microcache.log custom;</pre>

Notice the use of the <code>$upstream_cache_status</code> variable.

Excellent tutorial. Thank you.

I really like that also explain some of the inner workings and procided us with the purge script. It really helps understanding what is going on.

I get everything to work exactly as described but, I cannot get caching to work for my wordpress site. If I put time.php in my wordpress folder it gets cached but none of my other dynamic content from wordpress does.

Do you have any idea what might be the problem?

Below is my nginx virtual server for wordpress. I have removed the SSL settings which are standard and I have a SSL redirect server listening on port 80.

fastcgi_cache_path /etc/nginx/cache keys_zone=MYAPP:100m inactive=60m; fastcgi_cache_key “$request_method$host$request_uri”;

add_header X-Cache $upstream_cache_status;

server { listen 443 ssl; server_name example.com www.example.com; root /var/www/example.com;

    index index.php index.html index.htm;

    location / {
	rewrite ^/sitemap_index\.xml$ /index.php?sitemap=1 last; ## enables xml-site-maps
	rewrite ^/([^/]+?)-sitemap([0-9]+)?\.xml$ /index.php?sitemap=$1&sitemap_n=$2 last; ## enables xml-site-maps
	
	try_files $uri $uri/ /index.php?$args; ## enables pretty permalinks
            }

    error_page 404 /404.html;
   
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
           root /usr/share/nginx/html;
           }

    location ~ \.php$ {
           fastcgi_split_path_info ^(.+\.php)(/.+)$; #I have "cgi.fix_pathinfo = 0;" in php.ini
       
           fastcgi_pass unix:/var/run/php5-fpm.sock;
           fastcgi_index index.php;
           include fastcgi_params;

       fastcgi_cache MYAPP;
       fastcgi_cache_valid any 1m;
           }

    location ~ /\.ht {
           deny all;
           }

@micke The configuration directives are difficult to read please paste them at pastebin.com and share the link.

How do you say that caching is not working, did you check for the X-Cache HTTP header?

@jesin.a

Apparently, there was an update that garbled my post.

I found the problem with my set-up and I am posting it here for others that might experience the same problem. nginx will not cache pages that set cookies by default. Adding the following line fixed the problem:

fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

Where in the config file did you put this directive?

Place it above server {, also configure fastcgi to bypass the cache for requests to WP-Admin.

set $no_cache 0;

if ($request_method = POST) {
	set $no_cache 1;
}   
if ($query_string != "") {
	set $no_cache 1;
}   

if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
	set $no_cache 1;
}   

if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
	set $no_cache 1;
}

[...]
location ~ \.php$ {
	[...]
	fastcgi_cache_bypass $no_cache;
	fastcgi_no_cache $no_cache;
	[...]
}
[...]

FYI: md5 is not an encryption algorithm. It is a hash algorithm.

Great post. Thanks a lot!

Kamal Nasser
DigitalOcean Employee
DigitalOcean Employee badge
April 13, 2014

@a1099028: You’re correct, I’ll update the article. Thanks!

I found that cache works in some pages of my site, not all of them.

I created an advanced time.php, it can receive a value by GET then print it out. It works perfect and shows the “X-Cache: Hit” header. But some of my original programs still fail to cache. Why ?

(Cache:Hit) my time.php: http://55555.tw/time.php?var=1 (Cache:Miss) my original php: http://55555.tw/index.php?GoodsCountryID=1 (Cache:Hit) http://55555.tw (Cache:Miss) http://55555.tw/index.php

part of my nginx server setting: http://pastebin.com/vQHZZdbE

my fastcgi_params: http://pastebin.com/Gu1PLbCK

part of my php.ini : cgi.fix_pathinfo = 1

Is it possible to make the fastcgi_cache_valid time dynamic? For instance I’d like to cache some resources for much longer than others. I’ve tried a setup like the one below with no luck.

if ($request_uri ~* ^/ajax/getters/feed.php){ set $cache_time “5m”; } . . . fastcgi_cache_valid 200 301 302 304 $cache_time;

Andrew SB
DigitalOcean Employee
DigitalOcean Employee badge
April 18, 2014

@bmulley, Is the if statement inside of a location block?

My cache folder is owned by www-data:www-data. The permission on the folder is drwxrwsr-x

fastcgi_cache creates subfolders inside it with the permission drwx–S— and cache files have the permission -rw-------

nginx and PHP-FPM are running under www-data.

I cannot browse the cache subfolders using another user in www-data group. I can’t purge the contents of those subfolders via PHP. I get permission denied.

Do you know what I am doing wrong?

Thanks!

@Habib The subfolder permission says it all, drwx–S— means the group has NO permissions on the directory. But you should be able to purge it via PHP.

How are you executing the PHP purge script? Through the php command or through the web server?

My issue with this configuration is that the cache folder is always empty and nothing is being cached.

permission: drwxr-xr-x (755) owner/group: www-data www-data

Bonth nginx and PHP-FPM are running under www-data.

log_format custom ‘$upstream_cache_status’; The $upstream_cache_status returns "- ".

Kamal Nasser
DigitalOcean Employee
DigitalOcean Employee badge
July 24, 2014

@info: Try commenting all the cache exception if blocks and restarting nginx. Does it work with them disabled?

@Kamal Nasser

I removed almost all the config, leaving it on bare minimum. You can check my configs on: https://github.com/FabianGabor/nginx/

and also the time.php on with the rt-Fastcgi-Cache:MISS header: http://direct.fabiangabor.com/time.php

Does the purging script allow wildcard?

Eg, /mypath/*

If not, how can this be done?

I don’t think it is possible with the current cache implementation on Nginx. Cache filenames are in MD5 so choosing a list of URLs based on a wildcard is not possible.

There is a module named ngx_cache_purge which adds purging capability to Nginx so that we can purge URLs through HTTP requests. But even it doesn’t have this feature.

The only solution I know is to store all your URLs in the database, match the wildcard with PHP’s preg_match() function and purge all those URLs.

Hi Jesin,

Thanks for the tutorial. I have installed and tested it on a small server. The performance is awesome! My site does not have many updates a day so this is a great solution without using plugins in Wordpress. And now i can configure it how i want it.

(i have also compare your nginx solution with Apache and some cache plugins in Wordpress on the same small server, but nginx is the winner in my case).

Greetz,

Berto van Oorspronk.

hello How to enable “fastcgi_cache_path” because it shows me this, like this, this directive does not exist in my installation of nginx

nginx -t
nginx: [emerg] "fastcgi_cache_path" directive is not allowed here in /etc/nginx/nginx.conf:142
nginx: configuration file /etc/nginx/nginx.conf test failed```

thank you

Mich

hello

it is added on server {}

I was always the error message

Did I forget to add a function in me install nginx?

Type --with-

thank you

Mich

I am trying to set a separate cache for all login users.

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

After login to the CMS (Drupal), notice in X-Cache header in browser is displaying bypass. After commenting it out, X-Cache header is always a miss.

I am guessing the cache-control, expire headers are preventing pages the pages from being cached?

cache-control: no-cache, must-revalidate, post-check=0, pre-check=0

Are there any solutions to cache login users too?

Cheers! dropchew

Hello, when i logged in to my admin panel then for the time of caching all people that go to the website they see the admin panel on top of the page. Can we eliminate that?

It’s better to enable cache revalidation with the fastcgi_cache_revalidate directive. http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_cache_revalidate

Is this tutorial can be implemented in nginx as proxy in front of apache?

Or I have to use this [(https://www.digitalocean.com/community/tutorials/understanding-nginx-http-proxying-load-balancing-buffering-and-caching](http://)) to leverage nginx caching?

Thanks

This comment has been deleted

    Great stuff! I use Magento. I had to add:

    fastcgi_ignore_headers Cache-Control Expires Set-Cookie; 
    

    otherwise it wouldn’t cache. Is this actually correct?

    As I use magento I have a problem which I don’t know how to solve with:

    if ($request_uri ~* "/(page-I-dont-want-to-cache/|file-I-dont-want-to-cache.php)")
    {
        set $no_cache 1;
    }```
    
    
    My Problem: If I visit product A first, then it would be stored in the cache. If I then go to product B and add it into my shopping cart I get a little icon above the cart that indicates that I have one product in my cart. But when I go back to product A that was already cached, the little icon that indicates how many products I have in my cart isn't displayed anymore, That's because I am on a page that was stored in the cache before... 
    
    What could be done about this issue? 
    
    this is how my setup looks like for the URL's I don't want to cache:
    
    >

    if ($request_uri ~* “/(admin/|checkout/|customer/)”) { set $no_cache 1; }

    
    Thanks!

    Hi @jesin , thanks for great article. I am using wordpress. Is there a way to update post meta with this cache when post url is requested?

    This comment has been deleted

      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.