How to Set Up Varnish Cache as a Reverse Proxy on RHEL 7

Varnish Cache is a high-performance HTTP reverse proxy and caching accelerator designed to sit in front of your web server. When a visitor requests a page, Varnish checks whether it holds a fresh cached copy. If it does, the response is served entirely from RAM in microseconds — the origin web server is never contacted. On RHEL 7 systems running Nginx, placing Varnish on port 80 and moving Nginx to port 8080 is the standard topology. This guide covers installation from the official Varnish yum repository, writing a practical VCL configuration, overriding the systemd unit to listen on port 80, and using varnishstat and varnishlog to verify everything is working correctly.

Prerequisites

  • RHEL 7 server with root or sudo access
  • Nginx already installed and serving content on port 80 (we will move it to 8080)
  • Firewalld configured to allow traffic on ports 80 and 6082 (Varnish management)
  • At least 1 GB of free RAM to allocate to the Varnish cache
  • Active RHEL subscription or EPEL repository for any dependency packages

Step 1: Add the Varnish Yum Repository

The version of Varnish in the base RHEL 7 repositories is very old. The Varnish Software team provides a current repository for RHEL/CentOS 7:

sudo rpm --nosignature -i https://repo.varnish-cache.org/redhat/varnish-7.4.el7.rpm 2>/dev/null || 
  sudo curl -s https://packagecloud.io/varnishcache/varnish74/config_file.repo 
       -o /etc/yum.repos.d/varnish74.repo

# If the above repo is unavailable, use the EPEL version:
sudo yum install -y epel-release
sudo yum install -y varnish

For a production server, check https://packagecloud.io/varnishcache for the current repository URL for RHEL 7. Then install:

sudo yum install -y varnish
varnishd -V
# varnishd (varnish-7.x.x ...)

Step 2: Move Nginx to Port 8080

Before starting Varnish on port 80, reconfigure Nginx to listen on port 8080 so the two services do not conflict:

sudo vi /etc/nginx/nginx.conf

Change every listen 80 and listen [::]:80 directive to listen 8080 and listen [::]:8080. Also open server-specific config files in /etc/nginx/conf.d/ if they contain their own listen directives:

sudo grep -rn "listen 80" /etc/nginx/
# Edit each file found and change 80 to 8080

Allow SELinux to bind Nginx to a non-standard port:

sudo semanage port -a -t http_port_t -p tcp 8080

Reload Nginx:

sudo nginx -t && sudo systemctl reload nginx

Confirm Nginx is now on 8080:

ss -tlnp | grep 8080

Step 3: Write the Varnish VCL Configuration

Varnish is controlled by its Varnish Configuration Language (VCL). The default file lives at /etc/varnish/default.vcl. Replace its contents with a production-ready configuration:

sudo vi /etc/varnish/default.vcl
vcl 4.1;

import std;

# Backend definition — Nginx on localhost port 8080
backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout  = 5s;
    .first_byte_timeout = 60s;
    .between_bytes_timeout = 10s;
    .probe = {
        .url      = "/";
        .timeout  = 3s;
        .interval = 10s;
        .window   = 5;
        .threshold = 3;
    }
}

# Called for every incoming client request
sub vcl_recv {
    # Normalise the Host header to lowercase
    set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");

    # Strip port from X-Forwarded-For if present
    if (req.restarts == 0) {
        if (req.http.X-Forwarded-For) {
            set req.http.X-Forwarded-For =
                req.http.X-Forwarded-For + ", " + client.ip;
        } else {
            set req.http.X-Forwarded-For = client.ip;
        }
    }

    # Only cache GET and HEAD requests
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Do not cache requests with authorisation headers
    if (req.http.Authorization) {
        return (pass);
    }

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

    # Bypass for logged-in users (WordPress cookies)
    if (req.http.Cookie ~ "wordpress_logged_in|comment_author|wp-postpass") {
        return (pass);
    }

    # Remove cookies that do not affect page content (analytics, tracking)
    set req.http.Cookie = regsuball(req.http.Cookie,
        "(^|;s*)(_ga|_gid|_gat|_utm[a-z]+|__utm[a-z]+)=[^;]*", "");
    set req.http.Cookie = regsub(req.http.Cookie, "^;s*", "");

    # If no meaningful cookies remain, unset the header so Varnish will cache
    if (req.http.Cookie == "") {
        unset req.http.Cookie;
    }

    return (hash);
}

# Defines the cache hash (key)
sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    return (lookup);
}

# Called after the backend returns a response
sub vcl_backend_response {
    # Cache 404 pages for only 30 seconds to avoid storing errors long-term
    if (beresp.status == 404) {
        set beresp.ttl = 30s;
    }

    # Do not cache responses that tell us not to
    if (beresp.http.Cache-Control ~ "private|no-cache|no-store") {
        set beresp.uncacheable = true;
        set beresp.ttl = 120s;
        return (deliver);
    }

    # Cache HTML pages for 2 hours, static assets for 30 days
    if (beresp.http.content-type ~ "text/html") {
        set beresp.ttl = 2h;
    } else if (bereq.url ~ ".(css|js|png|jpg|jpeg|gif|ico|woff2|woff|svg)$") {
        set beresp.ttl = 30d;
    }

    # Strip Set-Cookie from cacheable responses so Varnish stores them
    if (beresp.ttl > 0s) {
        unset beresp.http.Set-Cookie;
    }

    return (deliver);
}

# Called before delivering the response to the client
sub vcl_deliver {
    # Add a header to indicate whether the response was a cache HIT or MISS
    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";
    }

    # Remove internal Varnish headers from the client-facing response
    unset resp.http.X-Varnish;
    unset resp.http.Via;

    return (deliver);
}

Step 4: Override the Systemd Unit to Listen on Port 80

By default on RHEL 7, Varnish listens on port 6081. We need to change this to port 80 using a systemd drop-in override — editing the unit file directly is not recommended as it will be overwritten on package upgrades:

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

Add the following content, replacing the ExecStart line entirely:

[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd 
    -a 0.0.0.0:80 
    -a 127.0.0.1:8443,PROXY 
    -f /etc/varnish/default.vcl 
    -s malloc,1G 
    -T 127.0.0.1:6082

Parameter notes:

  • -a 0.0.0.0:80 — listen on all interfaces, port 80
  • -s malloc,1G — allocate 1 GB of RAM for the object cache; adjust to suit your server
  • -T 127.0.0.1:6082 — management interface on localhost only (used by varnishadm)

Reload the systemd daemon and start Varnish:

sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo systemctl start varnish
sudo systemctl status varnish

Allow port 80 through the firewall if not already open:

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload

Step 5: Allow SELinux to Connect to the Backend

On RHEL 7 with SELinux enforcing, Varnish must be permitted to make network connections to Nginx on port 8080:

sudo setsebool -P varnishd_connect_any 1

Step 6: Test with varnishstat

varnishstat displays real-time counters for cache hits, misses, backend connections, and more:

sudo varnishstat

Key counters to watch:

  • MAIN.cache_hit — total requests served from cache
  • MAIN.cache_miss — requests that went to the backend
  • MAIN.n_object — number of objects currently in cache
  • MAIN.backend_req — total requests sent to Nginx

To see only the hit ratio in a one-shot snapshot:

sudo varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss

Step 7: Debug Requests with varnishlog

varnishlog streams a live log of Varnish transactions, invaluable for understanding why a particular URL is not being cached:

# Stream all transactions
sudo varnishlog

# Filter to a specific URL
sudo varnishlog -q 'ReqURL ~ "/my-page"'

# Show only the hit/miss decision and reason
sudo varnishlog -q 'VCL_call' -g request

If a URL shows MISS repeatedly, look for ReqHeader Cookie lines — an unexpected cookie is the most common cause of cache bypasses.

Step 8: Purge a Cached Object

Send a PURGE request via varnishadm or directly with curl (you must allow PURGE in VCL):

# Add this to vcl_recv in default.vcl to enable purging from localhost:
# if (req.method == "PURGE") {
#     if (client.ip == "127.0.0.1") { return (purge); }
#     return (synth(405, "Not allowed"));
# }

# Then reload VCL and purge:
sudo varnishadm vcl.load newconfig /etc/varnish/default.vcl
sudo varnishadm vcl.use newconfig

curl -X PURGE http://127.0.0.1/path/to/page

Conclusion

Varnish Cache transforms a standard Nginx-backed RHEL 7 server into a high-throughput edge cache capable of handling tens of thousands of requests per second from a single modest machine. The VCL language gives you precise, programmable control over every caching decision — stripping analytics cookies so anonymous pages are cached, bypassing authenticated sessions, and setting differentiated TTLs for dynamic HTML versus static assets. Use varnishstat to monitor your hit ratio over time and aim for 80% or higher on a content-heavy site. With the systemd override in place your configuration survives package upgrades cleanly, and SELinux is kept in enforcing mode throughout.