If you run a busy WordPress, WooCommerce, Laravel or custom PHP site, PHP and the database can become bottlenecks. You add OPcache, tune MySQL, maybe even move to a faster NVMe VPS, yet under peak load your application server still struggles. If much of that traffic is cacheable, putting an HTTP cache in front of your web server can substantially reduce application load. That is exactly where Varnish Cache shines as a reverse proxy in front of Nginx or Apache.
This guide covers Varnish in front of Nginx or Apache on a VPS or dedicated server, including architecture, listener changes, VCL, application exclusions and performance measurement. The examples target Varnish Cache 7.6 with VCL 4.1; distribution repositories may install a different version. Check the installed version and effective service configuration before adapting these examples.
Why Put Varnish in Front of Nginx or Apache?
Varnish is a high-performance HTTP accelerator designed to sit between clients (browsers, bots, APIs) and your origin web server. For cacheable pages, Varnish can store rendered responses and reuse them for subsequent requests, avoiding repeated PHP and database work.
When deployed correctly as a reverse proxy, Varnish can:
- Increase requests per second for cacheable pages by avoiding repeated PHP/MySQL processing.
- Reduce TTFB for cache hits by avoiding backend rendering time; the improvement depends on the workload and network path.
- Flatten traffic spikes during campaigns or news events by serving cache hits without repeatedly invoking PHP.
- Lower CPU and I/O usage on your Nginx/Apache+PHP stack, giving more headroom for dynamic operations like checkout, dashboards or APIs.
Varnish can complement other optimizations like correct PHP OPcache settings for WordPress and Laravel, tuned MySQL, and NVMe storage. These remain useful for cache misses and requests that must bypass the cache; size the VPS or dedicated server for that backend workload.
How Varnish Works as a Reverse Proxy
Before touching any configuration, it is important to understand how Varnish fits into the request flow and how its caching logic works.
Basic request flow
In a simple Varnish+Nginx setup, the flow typically looks like this:
- The client sends an HTTP request to your server's IP on port 80.
- Varnish listens on port 80 and receives the request.
- Varnish decides whether the request is cacheable, can be served from cache, or must go to the backend.
- If an eligible cached response is available, Varnish serves it from its configured storage; the malloc example below uses memory.
- On a miss or a deliberate pass, Varnish forwards the request to Nginx/Apache (the backend), typically listening on 8080 or 8081.
- Nginx/Apache generates the origin response, invoking PHP-FPM and database work where required, then returns it to Varnish.
- Varnish optionally stores the response in cache according to rules (TTL, Cache-Control headers, cookies), then sends it back to the client.
VCL: the brain of Varnish
Varnish is controlled by a configuration language called VCL (Varnish Configuration Language). With VCL you:
- Define backends (e.g. your Nginx or Apache instance).
- Decide which responses are safe to cache based on URLs, request methods and cookies.
- Implement cache bypass rules for logged-in users or cart pages.
- Control TTLs, grace periods, and how to handle errors.
If you are already comfortable writing Nginx configs or Apache vhosts, VCL will feel different but not scary: it is more like a mini-programming language for HTTP caching decisions.
What Varnish does (and does not) do
- Varnish does cache HTTP responses and handle routing to backends.
- Varnish does not handle HTTPS/TLS on its own. In production you usually terminate TLS with Nginx/Apache, HAProxy or Hitch and put Varnish behind that, or you put Varnish behind a CDN, with an origin-side TLS terminator if the CDN connects over HTTPS.
- Varnish is not a replacement for dedicated WAF or rate-limiting controls; consider Nginx rules, ModSecurity, or external services for those roles. See our Cloudflare security settings guide for examples of combining caching with WAF and bot protection.
Planning Your Varnish + Nginx/Apache Architecture
There are a few realistic ways to arrange Varnish with Nginx or Apache. The "right" choice depends on where you terminate HTTPS and whether you also use a CDN.
Scenario 1: Simple HTTP stack on a single VPS
This is the easiest starting point for labs, staging, or internal applications without public TLS:
- Client → Varnish on port 80
- Varnish → Nginx/Apache backend on port 8080
Nginx/Apache serves only HTTP internally. You do not expose it to the internet directly; Varnish is the single entry point.
Scenario 2: TLS termination in Nginx, Varnish behind it
Because Varnish does not speak TLS, a very common production pattern is:
- Client → Nginx (HTTPS 443)
- Nginx → Varnish (HTTP, local port e.g. 6081)
- Varnish → Nginx/Apache backend (HTTP 8080)
Nginx handles SSL/TLS. HTTP/2/3 and Brotli/Gzip compression depend on the installed build, modules and configuration; HSTS is an HTTP response policy. OCSP stapling also depends on the certificate authority and certificate; current Let’s Encrypt certificates do not support it. Varnish then operates on plain HTTP traffic. You can learn more about the HTTPS side in our guides on HTTP/2 and HTTP/3 and Brotli and Gzip compression settings.
Scenario 3: CDN in front, Varnish in the origin
A possible high-traffic architecture is:
- Client → CDN (TLS, HTTP/2/3)
- CDN → origin TLS terminator (HTTPS) → Varnish (HTTP), or CDN → Varnish over HTTP on a trusted private path
- Varnish → Nginx/Apache backend
The CDN offloads static assets and some HTML caching at the edge, while Varnish acts as an origin cache layer shielding your app servers. Restrict origin access to the intended ingress layer so that direct requests cannot bypass the CDN/WAF policy. This approach is common for multi-region architectures or when you also use GeoDNS and multi-region hosting.
Ports and processes
For the HTTP laboratory steps below, use this single-server layout:
- Varnish listens on HTTP port 80.
- Nginx or Apache listens only on 127.0.0.1:8080. In the separate HTTPS production layout, the TLS frontend on 443 proxies to Varnish on 127.0.0.1:6081, which forwards to the same backend.
For an Apache HTTP backend, move its port 80 listener to internal port 8080 and let Varnish take port 80. HTTPS still needs a separate TLS frontend on port 443, as in Scenario 2; moving that listener to 8443 does not make Varnish handle TLS.
Installing Varnish on a Typical Linux VPS
The following Ubuntu/Debian steps implement the HTTP-only laboratory layout with sudo/root access. They do not configure a public HTTPS frontend. Keep a copy of the working web-server and service configuration, and plan how to restore both if the transition fails. Package, service and file names differ on other distributions.
Step 1: Install Varnish
sudo apt update
sudo apt install varnish
This installs the distribution package. Run sudo varnishd -V, inspect its active listener and check the version’s documentation. Do not assume the package installs 7.6 or listens on a particular default port.
Step 2: Reconfigure Nginx to run on 8080
Edit your enabled Nginx site configurations. The excerpts below show only listener changes: retain the rest of each server block. Move every active port 80 listener that would conflict with Varnish, not just the default site. For example:
sudo nano /etc/nginx/sites-available/default
Change:
server {
listen 80 default_server;
listen [::]:80 default_server;
# Keep the rest of your existing server configuration here.
}
to:
server {
listen 127.0.0.1:8080 default_server;
# Keep the rest of your existing server configuration here.
}
The new listener is loopback-only. Review every existing IPv4 and IPv6 listener that would conflict with Varnish; do not leave another public 8080 listener enabled. Retain application routing, document root, PHP socket and access restrictions. Run the syntax check and reload only if it succeeds:
sudo nginx -t &&
sudo systemctl reload nginx
Step 3: Point Varnish to Nginx as backend
Edit the main VCL file (path may vary slightly by distribution):
sudo nano /etc/varnish/default.vcl
Update the existing default backend definition, or add it if absent; do not append a duplicate definition:
vcl 4.1;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
Varnish can now send misses and pass requests to localhost:8080. Check the correct virtual host with curl -H "Host: example.com" http://127.0.0.1:8080/. A backend on a different server needs an explicit private address and network access limited to Varnish; changing the port alone does not make it private.
Step 4: Make Varnish listen on port 80
Use a local systemd drop-in. The multi-user.target.wants entry can be an enablement symlink to a package unit; editing it can modify the vendor file. First inspect the unit, its drop-ins and its Type/ExecStart, then open a local override:
Check the actual paths and command before changing the listener:
sudo varnishd -V
sudo systemctl cat varnish
sudo systemctl show varnish -p Type -p ExecStart -p FragmentPath -p DropInPaths
sudo systemctl edit varnish
Conditional example: Use the following drop-in only if your current unit already uses Type=simple and exactly this foreground command with port 6081, with the secret file present and correctly permissioned. Otherwise copy your own complete ExecStart and change only the intended listener, preserving its jail, user, PID, secret and other package arguments. The empty ExecStart resets the previous command. -F runs in the foreground and must not be paired blindly with Type=forking; retain the package’s compatible Type/command pair.
[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd -F -a :80 -T 127.0.0.1:6082 -f /etc/varnish/default.vcl -S /etc/varnish/secret -s malloc,2G
The HTTP listener -a :80 belongs to this laboratory layout; the HTTPS frontend arrangement instead uses a local listener such as -a 127.0.0.1:6081. Keep the management listener local and authenticated with its secret. -s malloc,2G limits that object store to 2 GiB, not total process RAM or preallocated memory. Allow for object/thread overhead and Transient storage. Compile the VCL and continue only on success:
sudo varnishd -C -f /etc/varnish/default.vcl >/dev/null &&
sudo systemctl daemon-reload &&
sudo systemctl restart varnish &&
sudo systemctl status varnish --no-pager
If the service is healthy and the domain/network configuration is correct, test an actual GET response through the HTTP frontend:
curl -sS -D - -o /dev/null -H "Host: example.com" http://127.0.0.1/
Via and X-Varnish can show that a response passed through Varnish; neither alone proves a cache hit. Compare repeated GETs with varnishlog/statistics and verify content. In the HTTPS layout, preserve the intended Host and trusted scheme information at the frontend, and configure the application to trust only that proxy. Incorrect forwarding can select the wrong virtual host or cause HTTPS redirect loops. Restore both saved service and web-server settings if the transition fails.
Example with Apache as backend
With Apache, the concept is the same: change Apache to listen on 8080 and let Varnish take port 80.
sudo nano /etc/apache2/ports.conf
Modify:
Listen 80
to:
Listen 127.0.0.1:8080
In Apache 2.4, update the matching virtual hosts to <VirtualHost 127.0.0.1:8080>, retaining ServerName/ServerAlias and application settings. Remove conflicting listeners and run sudo apachectl configtest. Only on success, apply the listener change using your distribution’s service procedure; a stop/start may be needed for overlapping old/new bindings. Point Varnish at 127.0.0.1:8080 and verify the Host routing. NameVirtualHost is not required in Apache 2.4. HTTPS termination remains separate.
Tuning Varnish for Real-World Apps
Installing Varnish alone does not determine performance. Apply a policy that follows these rules, then measure it under your workload:
- Cache only public responses that the application permits a shared cache to reuse.
- Pass logged-in, cart, checkout, account and other personalized requests; a short TTL does not make shared caching safe.
- Choose static-asset freshness and invalidation deliberately; extensions alone do not prove that a response is public.
We already covered full-page caching strategies in our article Full-page caching for WordPress without breaking WooCommerce; here we'll focus on the Varnish-specific side.
Basic cache rules in VCL
Merge the following with the existing default backend, using one coherent vcl_recv and vcl_backend_response policy. The query-string bypass is deliberately conservative; add application-specific private/API routes and adapt paths for subdirectory or localized installations. Do not add an unconditional return(hash) or return(deliver) that skips the built-in safeguards:
sub vcl_recv {
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
if (req.url ~ "^/(wp-admin|wp-login[.]php|cart|checkout|my-account)(/|[?]|$)"
|| req.url ~ "[?]") {
return (pass);
}
# Fall through: built-in VCL passes Authorization and Cookie requests.
}
sub vcl_backend_response {
# Preserve private/no-store even if Surrogate-Control is also present.
if ((beresp.status != 200 && beresp.status != 301)
|| beresp.http.Cache-Control ~ "(?i)(no-cache|no-store|private)") {
set beresp.ttl = 0s;
}
# Caps only: never raise a zero/negative or shorter origin TTL.
if (bereq.url ~ "(?i)[.](png|jpe?g|webp|css|js|gif|svg)([?].*)?$") {
if (beresp.ttl > 1h) { set beresp.ttl = 1h; }
} else if (beresp.ttl > 120s) {
set beresp.ttl = 120s;
}
# No stale delivery in this baseline; enable only with a reviewed policy.
set beresp.grace = 0s;
# No return(deliver): run the built-in response safeguards next.
}
These are TTL caps: at most one hour for the listed extensions and 120 seconds otherwise. A shorter or nonpositive origin TTL is never raised. Built-in VCL still passes Cookie/Authorization requests and protects Set-Cookie, Vary:* and uncacheable responses. The extra Cache-Control check also preserves private/no-store/no-cache when Surrogate-Control exists. The built-in 120-second hit-for-miss marker does not mean that a private response body is cached for 120 seconds. Without origin freshness headers, default_ttl may apply; define application cacheability explicitly. This baseline disables stale delivery with grace=0s.
Microcaching vs long TTLs
Microcaching stores eligible shared responses briefly, often a few seconds. It can reduce repeated rendering for a busy public listing, but its value depends on request overlap and the application’s freshness requirement. Personalized content remains excluded regardless of TTL.
Insert this fragment at the end of the existing vcl_backend_response body, before built-in VCL runs. It relies on the earlier status/privacy checks. The public-feed path is an example and must be approved as public by your application policy:
# Insert at the END of the existing vcl_backend_response, before built-in VCL.
if (bereq.url ~ "^/public-feed(/|[?]|$)" && beresp.status == 200
&& beresp.ttl > 5s) {
set beresp.ttl = 5s;
}
# Keep baseline grace=0s unless stale delivery is explicitly authorized.
Grace permits stale delivery during renewal according to VCL policy. Varnish 7.6 initializes grace from stale-while-revalidate or default_grace, but this baseline resets it to zero. To enable it, define a permitted public route and bounded stale window; do not blindly override must-revalidate or private response policy. A stale-if-error header alone does not configure automatic error handling in this version. The documented background-fetch pattern if (beresp.status >= 500 && bereq.is_bgfetch) { return (abandon); } can avoid replacing a good stale object with a backend 5xx, but needs the rest of the grace policy. Do not cache an error response as a substitute. Keep is additional retention for conditional revalidation, not unlimited stale delivery.
The default VCL passes requests carrying Cookie; it does not automatically create a safe cache variant for each cookie value. A/B, language, currency and session cookies can affect the response and must not be discarded merely to improve hit rate.
Only after confirming that the three exact names below do not affect responses on an audited public-news route may you use this optional replacement vcl_recv. Add import cookie once at the top level; retain all your other private-path exclusions. Unknown and session cookies stay intact:
import cookie;
sub vcl_recv {
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
if (req.http.Authorization
|| req.url ~ "^/(wp-admin|wp-login[.]php|cart|checkout|my-account)(/|[?]|$)"
|| req.url ~ "[?]") {
return (pass);
}
# Only this application-audited public route may discard these cookies.
if (req.url ~ "^/public-news(/|$)" && req.http.Cookie) {
cookie.parse(req.http.Cookie);
cookie.delete("_ga");
cookie.delete("_gid");
cookie.delete("_fbp");
set req.http.Cookie = cookie.get_string();
if (req.http.Cookie == "") {
unset req.http.Cookie;
}
}
# Any remaining Cookie reaches the built-in pass rule.
}
The cookie VMOD deletes exact names, so a cookie such as _ga_extra is preserved. Any remaining Cookie falls through to the built-in pass rule. Do not replace this with a keep-list that silently deletes unknown state. Even when all these cookies are removed, response headers and other personalization still determine eligibility. Varnish pass does not bypass a CDN or application cache.
Bypassing sensitive paths
Always bypass caching on:
- Login pages
- Cart/checkout steps
- Account dashboards
- APIs that must always be fresh (e.g. stock levels, personalized feeds)
Keep the following condition before cookie removal or any early hash decision in the same vcl_recv. It already appears in the baseline. Include the actual localized cart, checkout and account paths, subdirectory prefixes, relevant APIs and plugin endpoints used by your store:
if (req.url ~ "^/(cart|checkout|my-account)(/|[?]|$)") {
return (pass);
}
Static assets: let Nginx shine
Although Varnish can cache static assets, you can route a clearly defined public-asset path directly to Nginx at the TLS frontend and optionally use a CDN. Set a long TTL only when versioned filenames or a reliable invalidation policy make it appropriate. Merely installing the HTTP chain above does not bypass Varnish for assets. Nginx can serve static files, especially when combined with Brotli or Gzip compression.
One practical model:
- A CDN may cache public versioned images, CSS and JavaScript for a longer period.
- Varnish can focus on eligible HTML or public JSON endpoints.
- Nginx/Apache remains the ultimate origin. When content changes, invalidate every affected layer; Varnish PURGE uses the hash key including Host and URL and removes matching Vary variants, while BAN applies to matching existing objects.
- Restrict invalidation to an authenticated/admin path. A loopback ACL alone is unsafe behind a local TLS proxy: public requests can also appear local. Block public PURGE at every frontend and verify updated content afterward.
Measuring and Proving Performance Gains
It is tempting to declare success as soon as you see Via: 1.1 varnish in response headers, but you will only know how much Varnish actually helps if you measure it carefully.
1. Baseline your site before enabling Varnish
Before you change anything, gather baseline numbers:
- TTFB and LCP from PageSpeed Insights, WebPageTest or Lighthouse.
- Average response time and RPS from a simple load test (k6, JMeter, wrk).
- CPU/RAM usage on your VPS or dedicated server under moderate load.
If you need a refresher on proper speed testing, see our article how to properly test your website speed.
2. Check cache hit ratio
Varnish exposes useful statistics through varnishstat and varnishlog. A key metric is cache hit ratio (hits vs misses) for your main pages.
sudo varnishstat -1 -f MAIN.cache_hit,MAIN.cache_miss,MAIN.cache_hitpass,MAIN.cache_hitmiss,MAIN.s_pass
Take two readings over the same test interval and use counter differences; discard intervals crossing a process restart. Δcache_hit/(Δcache_hit+Δcache_miss) is a ratio of these two counters, not the fraction of all HTTP traffic served from cache. Track pass/hit-for-pass/hit-for-miss separately and inspect specific URLs with varnishlog. A low ratio can be correct for private or fresh-only traffic. Investigate:
- Whether cookies represent real personalization or only explicitly audited tracking state.
- Whether TTL and Cache-Control reflect the application’s freshness and privacy requirements.
- Whether bypass routes match intended paths; do not remove a necessary exclusion to improve a metric.
3. Load-test with and without Varnish
On a staging environment, you can easily compare:
- Direct backend at 127.0.0.1:8080, accessed through a controlled test path.
- The corresponding Varnish frontend, keeping Host, scheme and application inputs equivalent.
Use a load-testing tool (for example k6, which we covered in our guide on load testing your hosting before traffic spikes) and run identical scenarios:
- Same application build, dataset, URL mix, Host/scheme and test duration.
- Comparable concurrency/request rate, separately recording warm-up and steady state.
- Separate public, logged-in, cart, query-string and other pass requests; do not report only warm hits.
- Record p95/p99 latency, RPS, error rate, backend load and memory; check that two different users never receive each other’s content.
For a workload with enough eligible cache hits, look for measurable changes in:
- Sustainable RPS at the same acceptable error rate.
- Median and tail latency, distinguishing hits from misses and pass requests.
- Origin CPU/I/O work and Varnish memory/evictions; an added layer can also add overhead.
4. Watch Core Web Vitals
Current Core Web Vitals are LCP, INP and CLS. TTFB is a supporting loading metric that can affect LCP; it is not itself a Core Web Vital. A warm cache may improve server response time, but frontend work, network and user devices still matter. Compare field data and controlled lab results separately; no cache configuration guarantees a ranking or Core Web Vitals outcome.
We dive deeper into the hosting side of Core Web Vitals in our article how server choices impact TTFB, LCP and CLS.
When Varnish Is (and Isn't) a Good Idea
Varnish is powerful, but it is not a silver bullet for every workload. Here is a realistic view of where it shines and where it might add unnecessary complexity.
Great use cases for Varnish
- Public content sites: eligible WordPress/blog/news pages with repeated reads; the cacheable proportion must be measured.
- Marketing pages: where freshness and invalidation can follow publishing or deployments.
- Hybrid stores: public catalog responses where cookies, location, currency and pricing do not personalize the cached output; cart/account/checkout remain excluded.
- Multi-tenant applications: default hashing includes Host and URL, but other tenant/user variants still need correct separation or pass. Preserve Host and validate allowed hostnames at the entry layer.
Less ideal or advanced scenarios
- Heavily personalized dashboards where almost every element is user-specific.
- Real-time applications relying on WebSockets or long-lived HTTP connections (chat, streaming data, etc.). Here, using Varnish requires careful exception rules or a different architecture.
- Complex APIs where clients depend on real-time responses. Microcaching can help but must be done very carefully.
In these cases, start by tuning PHP-FPM, OPcache, and your database first, and consider alternatives like Nginx microcaching or Redis object cache where their cacheability and invalidation policies fit; none makes private output safe merely by using a shorter TTL.
Planning a VPS or Dedicated-Server Deployment
For infrastructure selected from DCHost or another provider, check resource limits, administrative access and service scope, then plan the application stack together:
- Right-size the server (vCPU, RAM, NVMe storage) for the expected traffic and database size.
- Install and tune PHP-FPM and OPcache for the specific application (WordPress, WooCommerce, Laravel, custom PHP).
- Optimize MySQL/MariaDB or PostgreSQL, sometimes adding replication if the workload requires it.
- Configure Nginx or Apache properly (keep-alive, compression, protocol support according to the installed build and TLS frontend).
- Add Varnish as a reverse proxy in front of Nginx/Apache for shared-cache-eligible public responses; exclude personalized anonymous sessions too.
- Optionally place a CDN/WAF in front for global reach and additional protection.
Validate normal requests, private sessions, origin failures, content updates and rollback in staging. Cache hit rate is useful only alongside correct content isolation, freshness and acceptable error rates.
Summary and Next Steps
Varnish Cache can reduce repeated application work when a meaningful share of requests can safely reuse a shared response. The effect depends on traffic, freshness, object sizes and resources; the HTTP layer does not guarantee a particular TTFB or capacity gain.
Choose the HTTP/TLS topology, bind backends to the intended private path, preserve application routing, and apply VCL without bypassing built-in protections. Check the result using mixed workloads and separate hit/miss/pass measurements. Keep private responses out of every shared layer and verify invalidation before rollout.
Use the linked PHP, database, TLS, full-page caching and load-testing guides to assess the complete stack. For a VPS, dedicated server or colocation deployment, confirm the provider’s actual management scope and resources for your application. Adopt the additional cache layer when measured benefits justify its operational cost.





