🌐

Varnish

14 notes  •  Web Hosting

Install Varnish with cPanel and CentOS

This guide covers installing Varnish 4 as a caching layer in front of Apache on a cPanel/WHM CentOS server. Apache moves to port 8080 and Varnish listens on port 80.

Prerequisites

  • Root access to a CentOS 6/7 server running cPanel/WHM
  • Apache already serving your sites

Steps

1. Move Apache to port 8080

In WHM navigate to Home → Server Configuration → Tweak Settings, set Apache non-SSL IP/port to 8080, and save. Alternatively edit /etc/apache2/conf/httpd.conf directly.

2. Install Varnish

rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-4.0.el6.rpm
yum install varnish

3. Set Varnish to listen on port 80

Edit /etc/sysconfig/varnish and set:

VARNISH_LISTEN_PORT=80

4. Configure the VCL backend

Edit /etc/varnish/default.vcl and replace the backend block with your server IP. Adjust the IP to match your server.

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    if (req.url ~ "\.(png|gif|jpg|swf|css|js)$") {
        return(hash);
    }
}

# Strip cookies before caching static assets
sub vcl_backend_response {
    if (bereq.url ~ "\.(png|gif|jpg|swf|css|js)$") {
        unset beresp.http.set-cookie;
    }
}

5. Enable and start Varnish

chkconfig varnish on
service varnish start

Verify

Check that Varnish is listening on port 80 and proxying correctly:

varnishstat
curl -I http://127.0.0.1/

Notes

  • Test any VCL changes before restarting: varnishd -C -f /etc/varnish/default.vcl
  • Use varnishlog to inspect live request/response pairs.
  • Restart order: Apache first, then Varnish.

VCL Configuration for WordPress

A production-ready Varnish 4 VCL that caches WordPress pages while bypassing the cache for logged-in users, admin areas, and POST requests.

Prerequisites

  • Varnish 4.x installed and running
  • Apache/Nginx backend on 10.0.0.10:80 (adjust to your setup)

Steps

1. Replace /etc/varnish/default.vcl with the following

vcl 4.0;

backend default {
    .host = "10.0.0.10";
    .port = "80";
    .connect_timeout       = 600s;
    .first_byte_timeout    = 600s;
    .between_bytes_timeout = 600s;
    .max_connections       = 800;
}

# ACL for allowed PURGE sources
acl purge {
    "localhost";
    "127.0.0.1";
    "10.0.0.10";
}

sub vcl_recv {
    set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");

    # Allow cache purging only from the ACL above
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return(synth(405, "This IP is not allowed to send PURGE requests."));
        }
        return(purge);
    }

    # Do not cache authenticated or POST requests
    if (req.http.Authorization || req.method == "POST") {
        return(pass);
    }

    # WordPress: bypass RSS feeds, mu-plugins, admin, login
    if (req.url ~ "/feed")        { return(pass); }
    if (req.url ~ "/mu-.*")       { return(pass); }
    if (req.url ~ "/wp-(login|admin)") { return(pass); }

    # Strip tracking and WP-internal cookies
    set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?",              "");
    set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?",              "");
    set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?",               "");
    set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?",       "");
    set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?",  "");
    set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?","");

    # Unset cookie if now empty
    if (req.http.Cookie ~ "^ *$") { unset req.http.Cookie; }

    # Remove cookies from static asset requests
    if (req.url ~ "\.(css|js|png|gif|jp(e)?g|swf|ico)") {
        unset req.http.Cookie;
    }

    # Normalise Accept-Encoding
    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
            unset req.http.Accept-Encoding;
        } elsif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } elsif (req.http.Accept-Encoding ~ "deflate") {
            set req.http.Accept-Encoding = "deflate";
        } else {
            unset req.http.Accept-Encoding;
        }
    }

    # Pass through for logged-in WordPress users
    if (req.http.Cookie ~ "wordpress_" || req.http.Cookie ~ "comment_") {
        return(pass);
    }
    if (req.http.Cookie) { return(pass); }

    return(hash);
}

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    if (req.http.Accept-Encoding) {
        hash_data(req.http.Accept-Encoding);
    }
    return(lookup);
}

sub vcl_backend_response {
    unset beresp.http.Server;
    unset beresp.http.X-Powered-By;

    # Remove cookies from static assets
    if (bereq.url ~ "\.(css|js|png|gif|jp(e?)g|swf|ico)") {
        unset beresp.http.Set-Cookie;
    }

    # Only allow Set-Cookie for admin/login paths
    if (beresp.http.Set-Cookie && bereq.url !~ "^/wp-(login|admin)") {
        unset beresp.http.Set-Cookie;
    }

    # Do not cache POST or authenticated responses
    if (bereq.method == "POST" || bereq.http.Authorization) {
        set beresp.uncacheable = true;
        return(deliver);
    }
}

2. Test and reload

varnishd -C -f /etc/varnish/default.vcl
service varnish reload

Verify

Check that pages return an X-Cache: HIT header on the second request:

curl -sI http://example.com/ | grep -i x-cache

Notes

  • Extend cookie strip rules if you add WooCommerce or other plugins that set their own cookies.
  • Add the WordPress IP to the purge ACL so cache-busting plugins can send PURGE requests.

Varnish + Apache + HTTPS with Nginx SSL Termination

This guide sets up a three-layer stack: Nginx handles HTTPS on port 443, forwards plain HTTP to Varnish on port 80, and Varnish proxies cache misses to Apache on port 8080.

Prerequisites

  • Ubuntu/Debian server with Apache, Varnish, and Nginx installed
  • A valid SSL certificate (e.g. from Let's Encrypt)

Steps

1. Move Apache to port 8080

Edit /etc/apache2/ports.conf:

Listen 8080

Update every VirtualHost to use <VirtualHost *:8080>, then restart:

sudo service apache2 restart

2. Configure Varnish to listen on port 80

Edit /etc/default/varnish (or the systemd unit on Ubuntu 16.04+) and set the -a flag to :80:

DAEMON_OPTS="-a :80   -T localhost:6082   -f /etc/varnish/default.vcl   -s malloc,256m"

Edit /etc/varnish/default.vcl to point the backend at Apache:

vcl 4.0;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

3. Configure Nginx as the SSL termination proxy

Create or update your Nginx site configuration:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;

    location / {
        proxy_pass         http://127.0.0.1:80;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto https;
        proxy_set_header   X-Forwarded-Port  443;
        proxy_set_header   Host              $host;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

4. Restart all services

sudo service apache2 restart
sudo service varnish restart
sudo service nginx restart

Verify

# Confirm Varnish is receiving proxied HTTPS requests
curl -sI https://example.com/ | grep -i via

# Check all three ports are listening
ss -tlnp | grep -E '80|443|8080'

Notes

  • Apache must not listen on port 80 at all — Varnish owns that port.
  • Pass X-Forwarded-Proto: https from Nginx so your application can detect HTTPS even though the Varnish-to-Apache leg is plain HTTP.
  • On Ubuntu 16.04+ the /etc/default/varnish file is ignored by systemd; see the "Varnish daemon not listening on configured port" guide.

Configuring Varnish for High Availability with Multiple Backends

This guide configures Varnish to distribute traffic across multiple backend web servers using directors and health probes, so that Varnish automatically bypasses unhealthy nodes.

Prerequisites

  • Varnish 4.x installed
  • Two or more backend web servers reachable from the Varnish host
  • A health-check URL (e.g. /status.php) on each backend

Steps

1. Configure global Varnish options

Edit /etc/sysconfig/varnish (CentOS) or /etc/default/varnish (Ubuntu). Use malloc storage for best performance and tune thread pools to match your CPU count (one pool per core is a good baseline):

DAEMON_OPTS="-a :80   -T localhost:6082   -f /etc/varnish/default.vcl   -u varnish -g varnish   -S /etc/varnish/secret   -p thread_pools=4   -p thread_pool_min=100   -p thread_pool_max=4000   -s malloc,3G"

2. Define backends with health probes

Edit /etc/varnish/default.vcl:

vcl 4.0;

import directors;

# Health probe shared by all backends
probe healthcheck {
    .url       = "/status.php";
    .interval  = 5s;
    .timeout   = 1s;
    .window    = 5;
    .threshold = 3;
}

backend web1 {
    .host  = "192.168.1.10";
    .port  = "80";
    .probe = healthcheck;
}

backend web2 {
    .host  = "192.168.1.11";
    .port  = "80";
    .probe = healthcheck;
}

sub vcl_init {
    new cluster = directors.round_robin();
    cluster.add_backend(web1);
    cluster.add_backend(web2);
}

sub vcl_recv {
    set req.backend_hint = cluster.backend();
}

3. Test the VCL and reload

varnishd -C -f /etc/varnish/default.vcl
service varnish reload

Verify

# Check backend health status
varnishadm backend.list

# Watch live traffic distribution
varnishstat -f MAIN.backend_req

Notes

  • Change round_robin to random or fallback depending on your traffic pattern.
  • Use malloc storage (not file) whenever you have enough RAM; file-backed storage is significantly slower.
  • Set .threshold carefully — a value of 3 out of a window of 5 means the backend needs to fail 3 consecutive probes before being marked unhealthy.

Troubleshooting Varnish 503 Errors

A Varnish 503 "Service Unavailable / Guru Meditation" error means Varnish could not fetch a response from the backend. This guide covers the three most common causes and their fixes.

Prerequisites

  • Shell access to the Varnish host
  • Ability to edit /etc/varnish/default.vcl and the Apache config

Diagnosis: read the live log

Varnish keeps logs in memory only. Run this, then reload the failing page in a browser, and look for lines containing Error:

varnishlog

Fix 1 — Verify the backend is reachable

Check the port and host in your VCL:

grep -A4 "backend default" /etc/varnish/default.vcl

Then confirm the backend actually responds on that port from the Varnish host:

curl -I http://127.0.0.1:8080/

If it does not respond, fix Apache/Nginx first before troubleshooting Varnish.

Fix 2 — Increase backend timeouts

Slow dynamic pages can hit the default 1-second connect timeout. Raise them in /etc/varnish/default.vcl:

backend default {
    .host                  = "127.0.0.1";
    .port                  = "8080";
    .connect_timeout       = 5s;
    .first_byte_timeout    = 30s;
    .between_bytes_timeout = 10s;
}

Reload Varnish after saving:

service varnish reload

Fix 3 — Disable Apache KeepAlive

When Varnish reuses a persistent (KeepAlive) connection that Apache has quietly closed, the next request gets a 503 that appears almost instantly. The fix is to disable KeepAlive in /etc/apache2/apache2.conf:

KeepAlive Off

Then restart Apache:

sudo service apache2 restart

Verify

# Confirm backend is now healthy
varnishadm backend.list

# Watch for 503 counts dropping to zero
varnishstat -f MAIN.backend_fail

Notes

  • If 503s appear exactly at the connect timeout you set, the backend is not responding at all — check firewall rules and the backend service status.
  • KeepAlive Off is the right setting when Varnish sits between users and Apache; end-users connect to Varnish, not Apache, so Apache's KeepAlive gives no benefit.

Using the jemalloc VMOD for Memory Statistics

The libvmod-jemalloc module exposes jemalloc allocator statistics inside Varnish VCL, useful for diagnosing memory usage without stopping the service.

Prerequisites

  • Varnish 4.x built against jemalloc (the default on most distributions)
  • libvmod-jemalloc installed (available via the Varnish package repo or compiled from source at github.com/rezan/libvmod-jemalloc)

Steps

1. Import the module in your VCL

vcl 4.0;
import jemalloc;

2. Expose stats via a synthetic response

Add the following to your VCL. A request to /jemalloc returns the full stats as plain text; jemalloc stats are also printed to varnishlog on every request that carries a custom jemalloc header.

sub vcl_recv {
    if (req.url == "/jemalloc") {
        return(synth(200, "JEMALLOC"));
    }
}

sub vcl_synth {
    if (resp.status == 200 && resp.reason == "JEMALLOC") {
        set resp.http.Content-Type = "text/plain; charset=utf-8";
        synthetic(jemalloc.get_stats(options = "a"));
        return(deliver);
    }
}

sub vcl_deliver {
    if (req.http.jemalloc) {
        jemalloc.print_stats();
    }
}

3. Test and reload

varnishd -C -f /etc/varnish/default.vcl
service varnish reload

Verify

# Fetch jemalloc stats as plain text
curl http://127.0.0.1/jemalloc

# Print stats to varnishlog for a single request
curl -H "jemalloc: 1" http://127.0.0.1/
varnishlog -i VCL_Log

Notes

  • get_stats() requires approximately 50 KB of VCL workspace; ensure -p workspace_client is large enough if you see workspace errors.
  • Restrict the /jemalloc endpoint in production (e.g. with an IP-based ACL) to prevent exposing internal memory layout.
  • The options = "a" argument enables all jemalloc stat categories; omit it for the default summary.

HTTPS with Varnish using Nginx as SSL Terminator

Varnish does not support TLS natively. The standard solution is to place Nginx in front of Varnish as an SSL termination proxy: Nginx decrypts HTTPS on port 443 and forwards plain HTTP to Varnish on port 80, which then caches and proxies to the backend.

Prerequisites

  • Nginx installed on the same host or a dedicated proxy host
  • Varnish 4.x listening on port 80
  • A valid SSL certificate and key

Steps

1. Configure Nginx as the SSL terminator

Place the following in your Nginx site config (e.g. /etc/nginx/sites-available/example.com):

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;

    location / {
        proxy_pass         http://127.0.0.1:80;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto https;
        proxy_set_header   X-Forwarded-Port  443;
        proxy_set_header   Host              $host;
    }
}

2. Redirect HTTP to HTTPS inside Varnish VCL

Add the following to /etc/varnish/default.vcl so that plain HTTP requests hitting Varnish directly are redirected to HTTPS:

import std;

sub vcl_recv {
    # Redirect external HTTP requests to HTTPS
    if ((client.ip != "127.0.0.1" && std.port(server.ip) == 80) &&
        (req.http.host ~ "^(?i)(www\.)?example\.com")) {
        set req.http.x-redir = "https://" + req.http.host + req.url;
        return(synth(750, ""));
    }
}

sub vcl_synth {
    if (resp.status == 750) {
        set resp.status = 301;
        set resp.http.Location = req.http.x-redir;
        return(deliver);
    }
}

3. Test and restart services

varnishd -C -f /etc/varnish/default.vcl
sudo service varnish reload
sudo nginx -t && sudo service nginx reload

Verify

# Confirm HTTPS is served through Varnish
curl -sI https://example.com/ | grep -i via

# Confirm HTTP redirects to HTTPS
curl -sI http://example.com/ | grep -i location

Notes

  • HAProxy is an alternative to Nginx for SSL termination and supports PROXY protocol for accurate client IP forwarding.
  • The X-Forwarded-Proto: https header is critical so that PHP and other applications behind Varnish know the original request was HTTPS.
  • If you use the same Nginx instance as both the SSL terminator and a backend server, make sure it listens on a different port for backend traffic (e.g. 8080).

Diagnosing Varnish Cache Misses with Magento 2

When Magento 2 pages show X-Magento-Cache-Debug: MISS on every request, Magento is sending cache-control headers that prevent Varnish from storing the response. This guide identifies and fixes the most common causes.

Prerequisites

  • Magento 2 configured to use Varnish as the full-page cache (FPC)
  • Varnish VCL exported from Magento Admin via Stores → Configuration → Advanced → System → Full Page Cache → Export VCL for Varnish 4

Diagnosis

Step 1 — Check the response headers

curl -sI http://example.com/ | grep -iE 'x-magento|cache-control|via|x-varnish'

If you see Cache-Control: no-store, no-cache the problem is upstream of Varnish — Magento itself is marking the page uncacheable.

Step 2 — Find which layout XML block sets cacheable="false"

grep -rnw '/var/www/magento/app/code/' -e 'cacheable="false"'

Any extension that sets cacheable="false" on a layout block causes Magento to send no-store for the entire page.

Step 3 — Confirm the cache-control value changes when the block is disabled

Temporarily disable the offending module and repeat Step 1. If you now see Cache-Control: max-age=86400, public, s-maxage=86400, that extension is the cause.

Fix — Add Hit-For-Pass for uncacheable pages

If some pages are legitimately uncacheable, add the following at the end of sub vcl_backend_response in your Varnish VCL to avoid repeatedly fetching from the backend for those pages:

sub vcl_backend_response {
    # ...existing rules...

    # Bypass Varnish for 2 minutes for non-cacheable responses
    if (beresp.ttl <= 0s || beresp.http.Cache-Control ~ "no-store") {
        set beresp.ttl       = 120s;
        set beresp.uncacheable = true;
        return(deliver);
    }
}

Test and reload

varnishd -C -f /etc/varnish/default.vcl
service varnish reload

Verify

# Second request to a cacheable page should show HIT
curl -sI http://example.com/catalog/product/view/id/1   | grep -i 'x-magento-cache-debug'

Notes

  • Always verify with Magento's built-in FPC (without Varnish) first to confirm whether misses are a Varnish issue or a Magento cache issue.
  • The X-Magento-Cache-Debug: MISS header is only present in developer mode or when the Varnish VCL includes the debug header logic.
  • Third-party extensions are the most common cause; audit them before modifying core Magento files.

Improve WordPress Performance Using Varnish in Plesk Onyx

This guide installs Varnish as a Docker container in Plesk Onyx and inserts it between Nginx (port 80) and Apache, so cache hits are served from memory without hitting PHP.

Prerequisites

  • Plesk Onyx with Docker extension installed
  • Shell (SSH) access to the server
  • WordPress site managed by Plesk

Steps

1. Install the Docker extension in Plesk

Navigate to Plesk Home → Tools and Settings → Updates and Upgrades → Add/Remove Components. Select Docker and click Continue to install it.

2. Pull and run the Varnish Docker image

From the Plesk Docker extension, search for the official eeacms/varnish image and deploy it. Set the container port to 32780 (or another free port). Alternatively, from the shell:

docker pull eeacms/varnish
docker run -d --name varnish   -p 32780:6081   -e BACKENDS="127.0.0.1:7080"   -e BACKENDS_PORT="7080"   eeacms/varnish

Port 7080 is Plesk's default Apache non-SSL port.

3. Redirect Nginx traffic through Varnish

Edit the Nginx configuration for your domain. In Plesk, add an additional Nginx directive under Domains → example.com → Apache & Nginx Settings → Additional nginx directives:

location / {
    proxy_pass         http://127.0.0.1:32780;
    proxy_set_header   X-Real-IP       $remote_addr;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   Host            $host;
}

4. Apply and restart services

plesk repair web
service nginx restart

Verify

# Confirm Varnish container is running
docker ps | grep varnish

# Confirm Varnish is caching
curl -sI http://example.com/ | grep -i via
curl -sI http://example.com/ | grep -i x-cache

Notes

  • Varnish is not officially supported by Plesk; using Docker isolates it cleanly and makes rollback easy — just stop and remove the container.
  • Apache in Plesk listens on port 7080 (HTTP) and 7081 (HTTPS) by default; adjust the BACKENDS_PORT variable if your setup differs.
  • Add WordPress cookie-stripping rules to the Varnish VCL (see the VCL Configuration for WordPress article) to prevent logged-in user sessions from being cached.

WordPress DDoS Protection with Varnish 4 Rate Limiting

This guide installs the vsthrottle and shield VMODs for Varnish 4 and configures rate-limiting rules to block abusive request patterns against WordPress endpoints such as wp-login.php and xmlrpc.php.

Prerequisites

  • Ubuntu/Debian server with Varnish 4.0 or 4.1 already configured for WordPress
  • Build tools available (gcc, make, automake)

Steps

1. Install VMOD build dependencies

sudo apt-get install -y libvarnishapi-dev dpkg-dev pkg-config   build-essential git autotools-dev automake libtool

2. Install libvmod-shield

If you are on Varnish 4.1, change -b 4.0 to -b 4.1.

cd ~
git clone -b 4.0 https://github.com/varnish/libvmod-shield
cd libvmod-shield
sh autogen.sh
./configure
sudo make
sudo make install

3. Install libvmod-vsthrottle

cd ~
git clone -b 4.0 https://github.com/varnish/libvmod-vsthrottle
cd libvmod-vsthrottle
sh autogen.sh
./configure
sudo make
sudo make install

4. Add rate-limiting rules to your VCL

Edit /etc/varnish/default.vcl. Import both VMODs at the top:

vcl 4.0;
import vsthrottle;
import shield;

Inside sub vcl_recv, add the throttle rules:

sub vcl_recv {
    # Extract the real client IP (works with and without CloudFlare)
    set req.http.X-Actual-IP = regsub(req.http.X-Forwarded-For, "[, ].*$", "");

    # Block hammering of wp-login, xmlrpc, and search: max 2 req/s per IP
    if (vsthrottle.is_denied(req.http.X-Actual-IP, 2, 1s) &&
        (req.url ~ "xmlrpc|wp-login\.php|\?s\=")) {
        shield.conn_reset();
        return(synth(429, "Too Many Requests"));
    }

    # Block excessive POST requests outside admin-ajax: max 15 req/10s per IP
    if (vsthrottle.is_denied(req.http.X-Actual-IP, 15, 10s) &&
        (!req.url ~ "\/wp-admin\/|(xmlrpc|admin-ajax)\.php") &&
        req.method == "POST") {
        shield.conn_reset();
        return(synth(429, "Too Many Requests"));
    }

    # ... rest of your vcl_recv rules
}

5. Test and reload

varnishd -C -f /etc/varnish/default.vcl
sudo service varnish reload

Verify

Use Apache Bench to simulate rapid requests against the protected endpoint:

# Simulate 100 requests, 10 concurrent, directly to Varnish
ab -n 100 -c 10 http://127.0.0.1:80/wp-login.php

You should see Connection reset by peer errors confirming shield is active, or a high count of 429 responses.

Notes

  • Adjust the request thresholds (2, 1s and 15, 10s) to match your legitimate traffic patterns before deploying to production.
  • This is a complement to, not a replacement for, a proper WAF or DDoS-mitigation service like Cloudflare.
  • Varnish 4.1 branches of both VMODs are available; use -b 4.1 in the git clone commands if your Varnish version is 4.1.x.

Fix: Varnish Daemon Not Listening on Configured Port (Ubuntu 16.04+)

On Ubuntu 15.04 and later, systemd manages Varnish and ignores the legacy /etc/default/varnish file. Port and startup options must be set in the systemd service unit instead.

Prerequisites

  • Ubuntu 16.04 or later
  • Varnish installed via apt
  • Root or sudo access

Steps

1. Confirm the problem

systemctl status varnish
ss -tlnp | grep varnishd

If Varnish is running but not on your intended port, the systemd unit is overriding /etc/default/varnish.

2. Create a systemd drop-in override (preferred)

This method survives package upgrades without losing your changes:

sudo mkdir -p /etc/systemd/system/varnish.service.d
sudo nano /etc/systemd/system/varnish.service.d/customexec.conf

Paste the following, adjusting the port (-a :80) and memory (-s malloc,256m) as needed:

[Service]
ExecStart=
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,256m

The first ExecStart= (empty) clears the inherited value; the second sets the new command.

3. Reload systemd and restart Varnish

sudo systemctl daemon-reload
sudo systemctl restart varnish

Verify

systemctl status varnish
ss -tlnp | grep varnishd

Varnish should now be listening on port 80 (or whichever port you configured).

Troubleshooting

  • If you have conflicting files (e.g. an old customexec.conf left from a previous attempt), remove them and re-run daemon-reload.
  • As a last resort: purge and reinstall Varnish, then apply only the drop-in file described above:
    sudo apt remove varnish
    sudo apt-get purge varnish
    sudo apt install varnish
  • The authoritative systemd unit path is /lib/systemd/system/varnish.service — many older online tutorials point to /etc/init.d/varnish which is no longer effective on systemd systems.

Full Stack: Nginx (SSL) + Varnish (Cache) + Apache (Backend)

This guide configures a three-tier stack: Nginx terminates TLS on port 443 and proxies to Varnish on port 6081, Varnish caches and proxies cache misses to Apache on port 8080.

Prerequisites

  • Nginx, Varnish 4.x, and Apache installed
  • Let's Encrypt certificate already issued for the domain
  • Apache listening on port 8080 (not 80)

Steps

1. Varnish VCL — /etc/varnish/default.vcl

vcl 4.0;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Handle PURGE requests with optional regex ban
    if (req.method == "PURGE") {
        if (req.http.X-Purge-Method == "regex") {
            ban("req.url ~ " + req.url + " && req.http.host ~ " + req.http.host);
            return(synth(200, "Banned."));
        } else {
            return(purge);
        }
    }

    # Strip WordPress settings cookies (not login/auth cookies)
    set req.http.cookie = regsuball(req.http.cookie,
        "wp-settings-\d+=[^;]+(; )?", "");
    set req.http.cookie = regsuball(req.http.cookie,
        "wp-settings-time-\d+=[^;]+(; )?", "");
    set req.http.cookie = regsuball(req.http.cookie,
        "wordpress_test_cookie=[^;]+(; )?", "");
    if (req.http.cookie == "") { unset req.http.cookie; }

    # Bypass cache for WordPress admin and login
    if (req.url ~ "wp-admin|wp-login") { return(pass); }
}

sub vcl_backend_response {
    # Extend TTL if backend returns the default 2-minute TTL
    if (beresp.ttl == 120s) {
        set beresp.ttl = 1h;
    }
}

sub vcl_deliver {
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
}

2. Nginx site configuration

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    access_log /var/log/nginx/access.log;

    location / {
        proxy_pass         http://127.0.0.1:6081;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto https;
        proxy_set_header   X-Forwarded-Port  443;
        proxy_set_header   Host              $host;
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

3. Test and restart all services

varnishd -C -f /etc/varnish/default.vcl
sudo nginx -t

sudo service apache2 restart
sudo service varnish restart
sudo service nginx restart

Verify

# Confirm traffic path: Nginx -> Varnish -> Apache
curl -sI https://example.com/ | grep -iE 'via|x-cache|server'

# Confirm HTTP redirects to HTTPS
curl -sI http://example.com/ | grep location

Notes

  • Varnish listens on port 6081 in this stack (not 80), so Nginx must proxy to port 6081.
  • Make sure Apache's VirtualHosts use <VirtualHost *:8080> and that nothing else listens on 8080.
  • Replace placeholder IPs and domain names with your actual values before deploying.

VCL Configuration for Multiple Backends (Host-Based Routing)

This VCL routes requests to different backend servers based on the HTTP Host header, with separate TTL tuning, cookie handling, and HTTPS redirects for each domain.

Prerequisites

  • Varnish 4.x installed with the std module available
  • At least two backend servers reachable from the Varnish host

Steps

1. Deploy the multi-backend VCL

Replace /etc/varnish/default.vcl with the following, adjusting IPs, hostnames, and timeouts to match your infrastructure:

vcl 4.0;
import std;

# Backend definitions — adjust IPs and timeouts for your servers
backend backend_site1 {
    .host                  = "192.0.2.10";
    .port                  = "80";
    .connect_timeout       = 10s;
    .first_byte_timeout    = 600s;
    .between_bytes_timeout = 5s;
}

backend backend_site2 {
    .host                  = "192.0.2.20";
    .port                  = "80";
    .connect_timeout       = 5s;
    .first_byte_timeout    = 300s;
    .between_bytes_timeout = 5s;
}

sub vcl_recv {
    # Route by Host header
    if (req.http.host ~ "site1\.example\.com") {
        set req.backend_hint = backend_site1;
    } elsif (req.http.host ~ "site2\.example\.com") {
        set req.backend_hint = backend_site2;
    }

    # Redirect plain HTTP to HTTPS (skip if already from SSL terminator)
    if (client.ip != "127.0.0.1" && std.port(server.ip) == 80) {
        set req.http.x-redir = "https://" + req.http.host + req.url;
        return(synth(750, ""));
    }

    # Pass through WordPress previews
    if (req.url ~ "preview=true") { return(pass); }

    # Strip WordPress settings cookies; preserve auth cookies
    set req.http.cookie = regsuball(req.http.cookie,
        "wp-settings-\d+=[^;]+(; )?", "");
    set req.http.cookie = regsuball(req.http.cookie,
        "wp-settings-time-\d+=[^;]+(; )?", "");
    set req.http.cookie = regsuball(req.http.cookie,
        "wordpress_test_cookie=[^;]+(; )?", "");
    if (req.http.cookie == "") { unset req.http.cookie; }

    # Bypass cache for admin and login paths
    if (req.url ~ "wp-admin|wp-login") { return(pass); }

    # Handle PURGE requests
    if (req.method == "PURGE") {
        if (req.http.X-Purge-Method == "regex") {
            ban("req.url ~ " + req.url + " && req.http.host ~ " + req.http.host);
            return(synth(200, "Banned."));
        } else {
            return(purge);
        }
    }

    # Drop non-standard HTTP methods to pipe
    if (req.method !~ "^GET|HEAD|PUT|POST|TRACE|OPTIONS|DELETE$") {
        return(pipe);
    }
}

sub vcl_backend_response {
    set beresp.ttl   = 1h;
    set beresp.grace = 120s;

    # Do not cache error responses
    if (beresp.status == 404 || beresp.status >= 500) {
        set beresp.ttl   = 0s;
        set beresp.grace = 15s;
    }

    if (bereq.url ~ "wp-admin|wp-login") { return(pass); }
}

sub vcl_deliver {
    if (obj.hits > 0) {
        set resp.http.X-Cache      = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }
}

sub vcl_synth {
    if (resp.status == 750) {
        set resp.status = 301;
        set resp.http.Location = req.http.x-redir;
        return(deliver);
    }
}

2. Test and reload

varnishd -C -f /etc/varnish/default.vcl
service varnish reload

Verify

# Check each domain is routed to the correct backend
curl -sI -H "Host: site1.example.com" http://127.0.0.1/ | grep -i server
curl -sI -H "Host: site2.example.com" http://127.0.0.1/ | grep -i server

# Confirm cache headers
curl -sI http://site1.example.com/ | grep -i x-cache

Notes

  • Add a health probe to each backend definition in production (see the High Availability guide).
  • The X-Cache-Hits header shows how many times a cached object has been served — useful for confirming that caching is working across multiple requests.
  • The HTTPS redirect logic assumes Varnish sits behind an Nginx SSL terminator on 127.0.0.1; adjust the client.ip check if your topology differs.

Validate Varnish VCL Syntax Before Reloading

Before restarting or reloading Varnish after editing a VCL file, test the configuration for syntax errors. This is the Varnish equivalent of apachectl configtest.

Prerequisites

  • Varnish installed (varnishd in your PATH)
  • A VCL file to validate (default: /etc/varnish/default.vcl)

Steps

1. Run the syntax check

varnishd -C -f /etc/varnish/default.vcl

On success, Varnish prints the compiled C output of the VCL — a wall of code is expected and indicates no errors. On failure, an error message with a line number is printed to stderr and the command exits non-zero.

2. Reload Varnish if the check passes

# SysV / older Ubuntu
service varnish reload

# systemd (Ubuntu 15.04+, CentOS 7+)
systemctl reload varnish

Verify

# Confirm the running VCL is the one you intended
varnishadm vcl.list

Notes

  • Pipe the output through grep -i error for a quieter check:
    varnishd -C -f /etc/varnish/default.vcl 2>&1 | grep -i error
  • No output from the grep means the file is clean.
  • Use varnishadm vcl.load test01 /etc/varnish/default.vcl to load and compile a new VCL without making it active — useful for confirming a config in a running daemon before switching to it.