Varnish Cache Setup on a VPS: Speed Up Apache or Nginx
Your VPS specs look fine on paper, but pages still take 800ms-2s to load, and load testing makes things worse fast. If your site is mostly dynamic pages served fresh on every request, a caching layer like Varnish in front of Apache or Nginx can cut response times to single-digit milliseconds for cacheable traffic. Here's how to set it up without breaking logins, carts, or your SSL setup.
Symptom: Slow Pages Despite a Decent VPS
You check htop during a traffic spike and see PHP-FPM or Apache workers pegged at 100%, even though the box has 4+ cores and enough RAM. Time to First Byte (TTFB) creeps past 500ms on pages that haven't changed in days. Restarting services helps for a few minutes, then it's back.
This is the classic sign of a server doing full application work - database queries, PHP execution, template rendering - on every single request, including the tenth visitor loading the exact same homepage in the same minute.
Cause: No Caching Layer in Front of Your App
Apache and Nginx are good at serving requests, but by default they hand every request straight to your backend (PHP-FPM, a Node app, whatever). There's nothing sitting in front that remembers "I already built this page 10 seconds ago, here's the same response" - so the backend redoes the work every time.
Varnish is an HTTP accelerator that sits in front of your web server, holds a copy of responses in RAM, and serves repeat requests without touching the backend at all. It's not a WordPress plugin and it's not Apache/Nginx's built-in disk cache - it's a separate process that intercepts traffic before it ever reaches your app.
Where It Fits in the Stack
A typical layout on a single VPS looks like this:
Client → Varnish (port 80) → Apache/Nginx (port 8080) → PHP-FPM / app
Varnish takes over port 80. Your existing web server moves to an internal port like 8080 and only Varnish talks to it. HTTPS needs its own piece - Varnish doesn't terminate TLS - so either Nginx sits in front of Varnish as an SSL terminator, or you use something like Hitch. If you're already behind Cloudflare, Cloudflare can handle TLS to the visitor and you only need Varnish talking plain HTTP internally.
Fix: Installing and Configuring Varnish
1. Install Varnish
On Ubuntu/Debian:
sudo apt update
sudo apt install varnish
On AlmaLinux/CentOS/RHEL:
sudo dnf install epel-release
sudo dnf install varnish
2. Move Apache or Nginx Off Port 80
For Apache, edit /etc/apache2/ports.conf and change Listen 80 to Listen 8080, then update the matching <VirtualHost *:80> blocks to *:8080.
For Nginx, change listen 80; to listen 8080; in your server block. Restart the service afterward and confirm it's actually listening on the new port with ss -tlnp | grep 8080 before moving on - it's easy to edit the wrong vhost file and get stuck later wondering why Varnish shows a connection refused error.
3. Point Varnish at Your Backend
Edit /etc/varnish/default.vcl:
vcl 4.1;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
# Don't cache logged-in WordPress users or anyone with items in a cart
if (req.http.Cookie ~ "wordpress_logged_in|comment_author|woocommerce_items_in_cart") {
return (pass);
}
if (req.url ~ "^/wp-admin|^/wp-login.php|^/checkout|^/cart|^/my-account") {
return (pass);
}
unset req.http.Cookie;
}
sub vcl_backend_response {
if (bereq.url ~ "\.(css|js|jpg|jpeg|png|gif|svg|woff2?)$") {
set beresp.ttl = 7d;
} else {
set beresp.ttl = 2m;
}
}
That's a conservative starting point: static assets cache for a week, HTML pages cache for 2 minutes, and anything tied to a logged-in session or cart bypasses the cache completely. Tune the TTLs once you've confirmed nothing's broken - a news site might want a 30-second HTML TTL, a mostly-static brochure site can go much longer.
4. Set Varnish to Listen on Port 80
Edit the systemd override or /etc/default/varnish (Debian/Ubuntu) so the DAEMON_OPTS or ExecStart line uses -a :80 instead of the default :6081. Then:
sudo systemctl daemon-reload
sudo systemctl restart varnish
sudo systemctl status varnish
5. Confirm It's Actually Caching
curl -I http://yourdomain.com/
Look for an Age header greater than 0 on the second request, or add a custom debug header in your VCL (set resp.http.X-Cache = "HIT" / "MISS") so you can see it directly in the response. varnishstat gives you a live hit-rate dashboard, and varnishlog shows individual request decisions if something isn't caching the way you expect.
Common Problems and Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
| Logged-in users see cached/anonymous content | Session cookie not excluded in vcl_recv | Add your app's login cookie name to the pass rule and restart Varnish |
| WooCommerce cart shows wrong items | Cart/checkout URLs being cached | Exclude /cart, /checkout, /my-account explicitly |
| Varnish won't start, port already in use | Apache/Nginx still bound to port 80 | Re-check the vhost config and confirm with ss -tlnp |
| Content edits don't show up after publishing | Stale HTML still within its TTL | Lower the TTL or purge on save (see below) |
| SSL not working at all | Varnish has no TLS support built in | Put Nginx or Hitch in front for TLS termination, or offload TLS to Cloudflare |
Purging the Cache Automatically
Waiting out a 2-minute TTL is fine for most edits, but for WordPress a plugin like Varnish HTTP Purge sends a PURGE request to Varnish the moment a post or page is saved, so changes show up instantly instead of after the TTL expires. You'll need a small addition to vcl_recv to accept PURGE requests from localhost:
if (req.method == "PURGE") {
if (client.ip != "127.0.0.1") {
return (synth(405, "Not allowed."));
}
return (purge);
}
Prevention: Keep the Cache Layer Healthy
- Monitor hit ratio with
varnishstatweekly - a hit rate below 70% on mostly-static content usually means your TTLs are too aggressive or too many URLs are hitting the pass rules - Re-check your VCL exclusions any time you add a new plugin or feature that relies on sessions or cookies
- Keep a copy of your working
default.vcloutside/etc/varnishbefore making changes - a syntax error there stops Varnish from starting at all - Set
malloccache size in the Varnish service file to match available RAM, not the full disk - Varnish's default in-memory cache will evict old entries once it's full, which is normal
Varnish isn't a fit for every setup - if your site is mostly logged-in dashboard traffic (an intranet app, a members-only portal), there's not much to cache and you're better off tuning PHP-FPM workers or adding an object cache like Redis instead. But for anything public-facing with repeat anonymous traffic - blogs, marketing sites, WooCommerce catalog pages - it's one of the highest-impact changes you can make on a VPS that's starting to strain under load.
Frequently asked questions
Does Varnish work with WordPress out of the box?
Not quite out of the box - you need to exclude logged-in sessions, wp-admin, and any cart/checkout URLs from caching in your VCL, otherwise editors and shoppers will see stale or wrong content. Pairing Varnish with the Varnish HTTP Purge plugin handles automatic cache clearing on save.
Can I run Varnish and a plugin cache like LiteSpeed Cache or WP Rocket together?
You can, but it's usually redundant and can cause conflicts if both try to manage the same cache headers. Pick one layer - either Varnish in front of the whole stack, or a WordPress-level page cache plugin - and disable full-page caching in the other.
How do I handle HTTPS since Varnish doesn't support TLS?
Either put Nginx in front of Varnish to terminate SSL and proxy to Varnish on an internal port, use a dedicated TLS terminator like Hitch, or let Cloudflare handle HTTPS to visitors while Varnish only ever sees plain HTTP traffic between Cloudflare and your server.
Why is my hit ratio low even after setup?
Check for cookies being set on every response (a lot of themes/plugins set analytics or session cookies even for anonymous visitors), overly short TTLs, or URLs with tracking query strings (?utm_source=...) creating a separate cache entry per variant. Normalizing or stripping query strings in vcl_recv usually fixes this.
Will Varnish help if my bottleneck is the database, not PHP?
Yes, indirectly - since cached pages skip PHP and the database entirely, Varnish removes load from both. But if uncacheable pages (checkout, account dashboards, search results) are what's slow, you'll also want to look at MySQL slow query logs and indexing, since Varnish can't help with those requests.