For a project we're working on at Spatie we're expecting high traffic. That's why we spent some time researching how to improve the request speed of a Laravel application and the amount of requests a single server can handle. There are many strategies and services you can use to speed up a site. In our specific project one of the things we settled on is Varnish. In this post I'd like to share how to set up Varnish on a Forge provisioned server.

High level overview

First, let's discuss what Varnish does. Varnish calls itself a caching HTTP reverse proxy. Basically that means that instead of a webserver listening for requests, Varnish will listen for them. When a request comes in the first time it will pass it on to the webserver. The webserver will do its work as usual and send a response back to Varnish. Varnish will cache that response and send it back to the visitor of the site. The next time the same request comes in Varnish will just serve up it's cached response. The webserver (and PHP/Laravel) won't even be started up. This results in a dramatic increase in performance.

You might wonder how Varnish decides what should be cached and for how long. The answer: http headers. By default Varnish will look at the Cache-Control header in the response of the webserver. Let's take a look example of such a header. This header will let Varnish know that this response should be cached for 60 seconds.

Cache-Control: public , max-age= 60

Varnish can be configured in detail using a vcl . This stands or "Varnish Configuration Language". It can be used to manipulate the request. You can do things such as ignoring certain headers, removing cookies before they get passed to the webserver. The response coming back from the webserver can be manipulated as well. Think of things like manipulating or add headers to the response. There are more advanced features like Grace Mode and Health Checks can be configured as well.

The remainder of this blogpost aims to set up a basic varnish installation on a Forge provisioned server. Before you continue reading I highly recommended watching this presentation Mattias Geniar gave at this year's Laracon first. He explains some core concepts in more detail.

Prefer reading a blog post over watching a video? Then header over to this blogpost on Matthias' blog.

Installing Varnish

Now that you know what Varnish is and what it can do, let's get our handy dirty and install Varnish. If want to follow along provision a fresh server using Forge and install your project (or a clean Laravel installation) on it. I'm going to use the varnishtest.spatie.be domain for the remainder of this post. Replace this by your own url. Instead of using a clean Laravel installation I'm going to use a copy of our homegrown Laravel application template called Blender. By using Blender I'll hopefully run into many gotcha's when integrating Varnish with a real world app.

To install Varnish just run this command on your server:

sudo apt- get install varnish

This will pull in Varnish 4.x or Varnish 5.x. Both versions are fine.

Updating the default VCL

The VCL file - Varnish' configuration file - is located at /etc/varnish/default.vcl . Out of the box it contains some empty placeholders functions. To make Varnish behave in a sane way we should write out how we want Varnish to behave. But, once more, Mattias Geniar has got our backs. He open sourced his VCL configuration. He went the extra mile and put very helpful comments throughout the template. Go ahead and replace the contents of /etc/varnish/default.vcl with the content of Mattias' VCL.

Restart Varnish to make sure the updated VCL is being used:

sudo service varnish restart

Opening a port for Varnish

By default Varnish will listen for requests on port 6081. So the page that is seen when surfing to http://varnishtest.spatie.be (replace that url by the url of your project) is still being server up by nginx / PHP. Pointing the browser to http://varnishtest.spatie.be:6081 will result in a connection error. That error is caused by the firewall that by default blocks all requests to that port. In the Forge admin, underneath the "Network" tab you can add firewall rules. Go ahead and open up port 6081 by adding a line to that section.

Now surfing to http://varnishtest.spatie.be:6081 should display the same content as http://varnishtest.spatie.be:80 . If, in your project that page doesn't return a 200 response code, but a 302 you'll see an Error 503 Backend fetch failed error. In my multilingual projects "/" often gets redirected to "/nl", so I've encountered that error too. Let's fix that. Turns out the Mattias' VCL template uses a probe to determine if the back-end (aka the webserver) is healthy. Just comment out these lines by adding a # in front of it. A visit to http://varnishtest.spatie.be:6081 should now render the same content as on port 80 .

Checking the headers

Even though the content of the pages of port 80 and 6081 are the same the response headers are not. Here are the headers of my test application:

Notice those X-Cache and X-Cache-Hits headers? They are being set because we told Varnish to do so in our VCL. X-Cache is set to MISS . And it stays at that value when performing some more requests. So unfortunately Varnish isn't caching anything. Why could that be?

If you take another look at the headers you'll notice that cookie called laravel_session is being set. By default Laravel opens up a new session for every unique visitor. It uses that cookie to match up the visitor with the session on the server. In most cases this perfectly fine behaviour. Varnish however will, by default, not cache any pages where cookies are being set. A cookie being set tells Varnish that the response it received is for a specific visitor. That response should not be shown to another visitor.

Sure, we could try to configure Laravel in such a way that it doesn't set cookies, but that becomes cumbersome very quickly. To make working with Varnish in Laravel as easy as possible I've created a package called laravel-varnish. Simply put the package provides a middleware that sets a X-Cachable header on the response of every route the middleware is applied upon. In the Varnish configuration were going to listen for that specific header. If a response with X-Cacheable is returned from the webserver Varnish will ignore and remove any cookies that are being set.

Keep in mind that pages where you actually plan on using the session - to for example display a flash message or use authentication - cannot easily be cached with Varnish. There is a feature called Edge Side Includes with which you can cache parts of the page but that's out of scope for this blog post.

Making Varnish play nice with Laravel

As mentioned in the previous paragraph I've made a Laravel package that makes working with Varnish very easy. To install it you can simply pull the package in via Composer:

composer require laravel-varnish

After composer is finished you should install the service provider:

'providers' => [ ... 'Spatie\Varnish\VarnishServiceProvider' , ];

Next you must publish the config-file with:

php artisan vendor:publish --provider= "Spatie\Varnish\VarnishServiceProvider" --tag= "config"

In the published laravel-varnish.php config file you should set the host key to the right value.

Next the Spatie\Varnish\Middleware\CacheWithVarnish middleware should be added to the $routeMiddleware array:

protected $routeMiddleware = [ ... 'cacheable' => Spatie\Varnish\Middleware\CacheWithVarnish::class, ]

Finally, in you should add these lines to the vcl_backend_reponse function in your VCL located at /etc/varnish/default.vcl :

if (beresp.http.X-Cacheable ~ "1" ) { unset beresp.http.set-cookie; }

Restart Varnish again to make sure the added lines will be used:

sudo service varnish restart

Now that the package is installed we can apply that cacheable middleware to some routes.

Route::group([ 'middleware' => 'cacheable' ], function () { });

I've gone ahead and cached some routes in my application. Let's test it out by making a request to the homepage of the site and inspect the headers and response time.

You can see that the X-Cacheable header that was added on by our package. The presence of this header tells Varnish that it's ok to remove all headers regarding the setting of cookies to make this response cacheable. Varnish tells us through the X-Cache header that the response came not from it's cache. So the response was built up by PHP/Laravel. In the output you can also see that the total response time was a little under 200 ms.

Let's fire off the same request again.

Now the value of X-Cache header is HIT . Varnish got this response from its cache. Nginx / Laravel and PHP were not involved in answering this request. The total response time is now 75 ms, an impressive improvement.

To flush cache content normally you need to run a so-called ban command via the varnishadm tool. But with the package installed you can just use this artisan command on your server:

php artisan varnish:flush

You could use this command in your deployment script to flush the Varnish cache on each deploy. Running curl -I -w "Total time: %{time_total} ms

" http://varnishtest.spatie.be:6081/nl again will result in a cache miss.

Measuring the performance difference

The one small single test from the previous paragraph isn't really enough proof that Varnish is speeding up requests. Maybe we were just a bit lucky. Let's run a more thorough test. Blitz.io is an online service that can be used to perform load tests. Let's run it against our application.

We'll run this test:

It will run a bunch of get request originating from Ireland. In a time span of 30 seconds it will ramp up the load from 1 to 1000 concurrent users. This will result in about 26 000 request in 30 seconds.

The server my application is installed upon is the smallest Digital Ocean droplet. It has one CPU and 512 MB of RAM.

Let's first try running the test on port 80 where nginx is still listening for requests.

O my... The test runs fine for 5 seconds. After that the response time quickly rises. After 10 seconds, when 200 concurrent request / second are hitting the server response times grow to over 2 seconds. After 15 seconds the first errors start to appear due to the fact the webserver is swamped in work. It starts to send out 502 Bad Gateway errors. From there on the situation only gets worse. Nearly all requests result in errors or timeouts.

Let's now try the same test but on port 6081 where Varnish is running.

What. A. Rush. Beside the little hiccup at the end, the response time of all requests was around 80 ms. And even the slowest response was at a very acceptable 120 ms. There were no errors: all 26 000 requests got a response. Quite amazing.

Let's try to find Varnish' breaking point and up the concurrent requests from 1 000 to 10 000.

15 seconds in, with 2 500 requests hitting on our poor little server in one second, the response time rises to over a second. But only after nearly 30 seconds, with a whopping 6 000 requests a second going on, Varnish starts returning errors.

Installing Varnish: final steps

As it stands, Varnish is still running on port 6081 and nginx on port 80. So all the visitors of our site still get served uncached page. To start using Varnish for real the easiest thing your can to is to just swap the ports. Let's make Varnish run on port 80 and nginx on port 6081.

Here are the steps involved:

edit /etc/nginx/sites-enabled/<your domain> . Change

listen 80 ; listen [::]:80;

to

listen 6081 ; listen [::]:6081;

edit /etc/nginx/forge-conf/<your domain>/before/redirect.conf . Change

listen 80 ; listen [::]:80;

to

listen 6081 ; listen [::]:6081;

edit /etc/nginx/sites-available/catch-all . Change

listen 80 ;

to

listen 6081 ;

edit /etc/default/varnish . Change

DAEMON_OPTS="-a :6081 \

to

DAEMON_OPTS="-a :80 \

edit /etc/varnish/default.vcl . Change

.port = "80" ;

to

.port = "6081" ;

edit /lib/systemd/system/varnish.service . Change

ExecStart= /usr/ sbin/varnishd -j unix,user=vcache -F -a : 6081 -T localhost: 6082 -f /etc/varnish/ default .vcl -S /etc/varnish/secret -s malloc, 256 m

to

ExecStart= /usr/ sbin/varnishd -j unix,user=vcache -F -a : 80 -T localhost: 6082 -f /etc/varnish/ default .vcl -S /etc/varnish/secret -s malloc, 256 m

I'm pretty sure there must be a more elegant way, but let's just fire off the bazooka and restart the server. When it comes back up you'll notice that headers of response on port 80 will contain X-Cache headers (set by Varnish) and those on port on 6081 (where Nginx is running) do not. If you wish to do so may remove the line that opens up port 6081 in Firewall rules screen on Forge.

And with that your Varnish installation should be up and running. Congratulations!

In closing

Varnish is a incredible powerful tool that can speed up your application immensely. In this post I've barely scratched the surface of what you can do with it. If you want to learn more about it, check out the official documentation or the aformentioned blogpost by Mattias Geniar.

Out of the box Varnish can't handle https connections, so you'll need to do some extra configurating to make it work.

For our particular project we settled on Varnish, but there are plenty alternatives to scale your app:

Also keep in mind that you should only use Varnish if you expect high traffic. For smallish sites you probably shouldn't bother installing and configuring Varnish.

If you do decide to use Varnish, be sure to take a look at our laravel-varnish package. If you like it, check out our other Laravel and PHP packages.