How to Install Varnish Cache on Ubuntu 24.04 VPS: Full-Page HTTP Accelerator
Varnish Cache turns a modest VPS into a response machine. By serving cached HTML straight from RAM, a single 4 vCPU instance can answer tens of thousands of requests per second while the origin application sleeps. This guide walks you through installing Varnish Cache 7.4 LTS on Ubuntu 24.04, layering it behind an Nginx TLS terminator, writing a production VCL for WordPress, wiring up cache purging, and troubleshooting the failure modes you are most likely to hit.
Need a cache-ready VPS? Our CloudCore Professional plans ship with the RAM headroom Varnish loves -- deploy in 60 seconds and point your domain at it.
Table of Contents
What is Varnish Cache?
Varnish Cache is an HTTP reverse proxy designed from the ground up as a full-page cache. It sits in front of your application server (Apache, Nginx, Node.js, PHP-FPM via an intermediate web server), stores complete HTTP responses in memory, and serves subsequent identical requests without ever touching the origin. A Varnish process can push well over 100,000 requests per second on commodity hardware, which is one to two orders of magnitude more than a typical dynamic PHP stack can manage.
The defining feature of Varnish is VCL (Varnish Configuration Language), a domain-specific language that compiles to C and then to a shared object loaded into the running daemon. VCL is not a list of key/value options -- it is a set of subroutines (vcl_recv, vcl_backend_response, vcl_deliver, and others) that you write to describe exactly how requests and responses flow through the cache. This makes Varnish extraordinarily flexible: you can strip cookies, rewrite URLs, pick backends by geolocation, enforce per-path TTLs, vary responses on custom headers, and invalidate cache entries over HTTP -- all in a few dozen lines of config.
Typical production use cases include accelerating WordPress, Magento, and Drupal sites where rendered HTML is expensive to generate; fronting REST and GraphQL APIs to absorb read traffic spikes; caching static asset CDNs before they reach object storage; and protecting origin application servers from Slashdot-effect traffic bursts. Large publishers (The Guardian, The New York Times, Wikipedia) have used Varnish for more than a decade as the first tier after their TLS terminator.
Why Use Varnish Instead of Nginx Microcache?
Nginx includes a built-in proxy_cache module that can absolutely do full-page caching, and for small WordPress sites it is often enough. Varnish earns its place once traffic or cache complexity grows.
- Much higher TPS on the same hardware -- Varnish's shared-memory log, epoll-based worker threads, and purpose-built storage engine consistently out-run Nginx microcache in head-to-head benchmarks, particularly under high cache-hit concurrency. Independent tests regularly show 2-5x more requests per second on identical VMs.
- VCL is a real programming language -- Nginx cache rules are declarative directives. Varnish gives you conditionals, regexes, string manipulation, sub-routines, and a plugin system (VMODs). Complex cache keys, header-based routing, A/B testing logic, and edge-side includes are all straightforward.
- Native cache invalidation -- Varnish has first-class support for
PURGEandBANHTTP verbs protected by ACLs. Nginx requires the third-partyngx_cache_purgemodule or shell tricks against the filesystem. - Per-object TTL based on backend response -- In VCL you decide caching policy after seeing the backend's headers, which makes it trivial to give
/api/productsa 30-second TTL while/blog/*gets 1 hour. - varnishlog is a superpower -- Every request emits a structured, queryable log stream in shared memory. You can filter by VCL tag, HTTP header, or client IP in real time without touching disk I/O.
Architecture: Nginx → Varnish → Backend
Varnish does not handle TLS natively. The recommended production topology is:
Client
│ HTTPS :443
▼
┌─────────┐
│ Nginx │ TLS termination, HTTP/2, static files
└────┬────┘
│ HTTP :6081
▼
┌─────────┐
│ Varnish │ Full-page HTTP cache (RAM)
└────┬────┘
│ HTTP :8080
▼
┌─────────┐
│ Backend │ Apache / Nginx origin / PHP-FPM
└─────────┘- Nginx on :443 handles TLS, HTTP/2, ACME renewals, and forwards cleartext HTTP to Varnish on the loopback.
- Varnish on :6081 caches full HTTP responses in memory and, on miss, forwards the request to the origin on port 8080.
- Backend on :8080 is your existing web server (Apache for WordPress, Nginx for a static site, Node.js, etc.). It is no longer directly exposed to the internet.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- A working backend application (WordPress, a LEMP stack, or similar) currently listening on port 80 or 443
- A domain name already pointing to the server's IP (A/AAAA records via Cloudflare or your DNS provider)
- At least 2 GB of RAM -- Varnish is happiest when you can give it 1 GB or more of dedicated cache memory
- At least 2 vCPU cores -- Varnish parallelises aggressively
Recommended Plan: CloudCore Professional>
Varnish's performance scales with RAM and CPU. For a cache in front of a medium-traffic WordPress or Magento site, we recommend the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM (plenty for a 4 GB malloc cache plus the application stack)
- 100 GB NVMe SSD
- Unmetered bandwidth>
The NVMe disk matters if you choose the file storage backend, and the extra RAM lets you keep both PHP-FPM and a sizeable cache warm simultaneously.Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
sudo apt update && sudo apt upgrade -yInstall the helper packages we will need for the repository setup:
sudo apt install -y curl gnupg debian-archive-keyring apt-transport-https ca-certificatesIf the kernel was upgraded, reboot before continuing:
sudo rebootStep 2: Add the packagecloud.io Repository
Ubuntu 24.04 ships with Varnish 7.5 in the universe repository, but the version stream on packagecloud.io is the one the Varnish project maintains officially. It gives you Varnish 7.4 LTS (supported until 2026) plus timely security patches.
Import the repository signing key:
curl -fsSL https://packagecloud.io/varnishcache/varnish74/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/varnish74-archive-keyring.gpgAdd the repository definition:
sudo tee /etc/apt/sources.list.d/varnishcache_varnish74.list > /dev/null <<'EOF'
deb [signed-by=/usr/share/keyrings/varnish74-archive-keyring.gpg] https://packagecloud.io/varnishcache/varnish74/ubuntu/ noble main
deb-src [signed-by=/usr/share/keyrings/varnish74-archive-keyring.gpg] https://packagecloud.io/varnishcache/varnish74/ubuntu/ noble main
EOFPin the upstream packages so apt upgrade does not silently replace them with the Ubuntu version:
sudo tee /etc/apt/preferences.d/varnish > /dev/null <<'EOF'
Package: varnish varnish-*
Pin: origin packagecloud.io
Pin-Priority: 1000
EOFRefresh the package index:
sudo apt updateStep 3: Install Varnish 7.4 LTS
sudo apt install -y varnishConfirm the installed version:
varnishd -VExpected output:
varnishd (varnish-7.4.3 revision ...)
Copyright (c) 2006 Verdens Gang AS
Copyright (c) 2006-2024 Varnish SoftwareCheck that the service is running:
sudo systemctl status varnishThe default configuration listens on port 6081 and proxies to a backend on 127.0.0.1:8080. That matches the architecture above, so we only need to tune a few parameters and write our VCL.
Step 4: Configure the systemd Listen Parameters
Varnish is controlled by systemd unit parameters rather than a config file -- you pass flags directly to varnishd. On Ubuntu the unit lives at /lib/systemd/system/varnish.service, but you should never edit that directly. Instead, create an override:
sudo systemctl edit varnishIn the editor, add:
[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd \
-a :6081 \
-a localhost:6082,PROXY \
-p feature=+http2 \
-f /etc/varnish/default.vcl \
-s malloc,1g \
-T localhost:6083 \
-S /etc/varnish/secretSave and exit. The blank ExecStart= line is required -- it clears the inherited default before you set a new one.
What each flag does:
-a :6081-- Listen on port 6081 for plain HTTP from the Nginx TLS terminator.-a localhost:6082,PROXY-- Additional listener that speaks the PROXY protocol. Useful if you later front Varnish with HAProxy or a load balancer that preserves client IPs.-p feature=+http2-- Enable HTTP/2 on the backend-facing connection.-f /etc/varnish/default.vcl-- Path to the VCL file we will write in the next step.-s malloc,1g-- The storage engine and size.mallockeeps the cache entirely in RAM, which is the fastest option and right for most sites. Use-s file,/var/lib/varnish/cache.bin,20gif you need a cache larger than your available RAM (the kernel page cache will still hold hot objects in memory).-T localhost:6083-- Admin/CLI port forvarnishadm.-S /etc/varnish/secret-- Shared secret file used to authenticatevarnishadmsessions.
malloc vs file storage
malloc,SIZEis RAM-backed. It is the fastest, simplest, and what you almost always want. The OS kernel is not involved in serving cached objects. If Varnish is killed, the cache is lost.file,PATH,SIZEis disk-backed but mmap'd into memory. On an NVMe VPS with modest RAM, this lets you hold a 50 GB catalogue of rarely-changing pages. It is slower than malloc on a cold cache but fine once the working set is in page cache.default,SIZEis a synonym for malloc in recent Varnish versions.
file on a spinning disk -- the seek latency defeats the point of a cache.Reload systemd and restart Varnish:
sudo systemctl daemon-reload
sudo systemctl restart varnishStep 5: Write a Production VCL
Varnish ships with a trivial default VCL at /etc/varnish/default.vcl. Replace it with a WordPress-tuned version that demonstrates the important subroutines.
sudo cp /etc/varnish/default.vcl /etc/varnish/default.vcl.orig sudo tee /etc/varnish/default.vcl > /dev/null <<'EOF' vcl 4.1;import std;
backend default { .host = "127.0.0.1"; .port = "8080"; .connect_timeout = 5s; .first_byte_timeout = 60s; .between_bytes_timeout = 30s; }
ACL for clients allowed to issue PURGE requests.
acl purge { "localhost"; "127.0.0.1"; "::1"; }sub vcl_recv { # Set the forwarded-for header for the backend. 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; } }
# Allow cache invalidation over HTTP PURGE. if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(403, "Purge not allowed")); } return (purge); }
# Only cache GET and HEAD. if (req.method != "GET" && req.method != "HEAD") { return (pass); }
# Never cache the WordPress admin, login, or REST API writes. if (req.url ~ "^/wp-(login|admin|cron)" || req.url ~ "^/xmlrpc\.php" || req.url ~ "preview=true") { return (pass); }
# Skip cache for logged-in users and commenters. if (req.http.Cookie ~ "wordpress_logged_in_" || req.http.Cookie ~ "comment_author_" || req.http.Cookie ~ "wp-postpass_" || req.http.Cookie ~ "woocommerce_items_in_cart") { return (pass); }
# Strip tracking and analytics cookies so anonymous visitors share a cache key. set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_[_a-z]+|__utm[a-z]+|_ga|_gid|_fbp|wp-settings-\d+|wp-settings-time-\d+)=[^;]+", ""); set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
# If nothing useful is left, drop the Cookie header entirely. if (req.http.Cookie == "" || req.http.Cookie ~ "^\s*$") { unset req.http.Cookie; }
# Normalise Accept-Encoding so we do not cache the same page twice. if (req.http.Accept-Encoding) { if (req.url ~ "\.(jpg|jpeg|png|gif|gz|mp3|mp4|zip|ico|webp|avif|pdf)$") { unset req.http.Accept-Encoding; } elsif (req.http.Accept-Encoding ~ "br") { set req.http.Accept-Encoding = "br"; } elsif (req.http.Accept-Encoding ~ "gzip") { set req.http.Accept-Encoding = "gzip"; } else { unset req.http.Accept-Encoding; } }
return (hash); }
sub vcl_backend_response { # Set a default TTL if the backend did not send Cache-Control. if (beresp.ttl <= 0s) { set beresp.ttl = 1h; set beresp.uncacheable = false; }
# Cache static assets aggressively regardless of origin headers. if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp|avif)$") { unset beresp.http.Set-Cookie; set beresp.ttl = 7d; set beresp.http.Cache-Control = "public, max-age=604800"; }
# Enable grace mode -- serve stale content for up to 6 hours while we refresh. set beresp.grace = 6h;
# Do not cache responses that set cookies (login, checkout, etc.). if (beresp.http.Set-Cookie) { set beresp.uncacheable = true; }
return (deliver); }
sub vcl_deliver { # Debug header so you can tell from the client if a response was cached. 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 headers that leak the backend stack. unset resp.http.X-Powered-By; unset resp.http.Server; unset resp.http.Via;
return (deliver); } EOF
Validate the VCL before reloading -- a syntax error leaves the old config running, but it is still good hygiene:
sudo varnishd -C -f /etc/varnish/default.vcl > /dev/nullIf there is no output, the VCL compiled successfully. Reload Varnish live (no downtime):
sudo varnishreloadWhat this VCL does
vcl_recvruns on every incoming request. We check forPURGE, strip anonymous-visitor cookies, honour logged-in WordPress users, and normaliseAccept-Encodingso we do not keep separate cache entries forgzip, deflate, brvsgzip, br.vcl_backend_responseruns after the backend answers a miss. We force a 1 hour default TTL, give static assets 7 days, mark any response withSet-Cookieas uncacheable, and enable 6 hours of grace mode so stale pages continue to serve while Varnish asynchronously refreshes them.vcl_deliverruns just before we ship the response to the client. We add anX-Cache: HIT|MISSheader that is invaluable for debugging, and strip server-fingerprint headers.
Step 6: Put Nginx in Front of Varnish for TLS
Varnish speaks cleartext HTTP only. For HTTPS you need a TLS-terminating frontend. Nginx is the pragmatic choice -- it handles HTTP/2, certificate renewal via Certbot, and can still serve static files directly if you want to bypass Varnish for specific paths.
If you do not already have our dedicated article: How to Install Nginx on Ubuntu 24.04 walks through the initial Nginx setup. Assuming Nginx is already installed, install Certbot:
sudo apt install -y certbot python3-certbot-nginxCreate the site configuration:
sudo tee /etc/nginx/sites-available/varnish-frontend > /dev/null <<'EOF' server { listen 80; listen [::]:80; server_name yourdomain.com www.yourdomain.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off;
# Let Varnish see the real client IP and scheme. location / { proxy_pass http://127.0.0.1:6081; proxy_set_header Host $host; 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 $scheme; proxy_http_version 1.1; proxy_set_header Connection ""; } } EOF
Enable the site and obtain a certificate with Let's Encrypt (see our Let's Encrypt guide if you are new to Certbot):
sudo ln -s /etc/nginx/sites-available/varnish-frontend /etc/nginx/sites-enabled/
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
sudo nginx -t && sudo systemctl reload nginxStep 7: Move the Backend to Port 8080
Your origin application (Apache or a second Nginx vhost serving WordPress) is currently probably bound to :80 or :443. It needs to move to 127.0.0.1:8080 so Varnish can proxy to it.
For an Apache backend
Edit /etc/apache2/ports.conf:
Listen 127.0.0.1:8080Edit your vhost in /etc/apache2/sites-available/*.conf:
<VirtualHost 127.0.0.1:8080>
ServerName yourdomain.com
DocumentRoot /var/www/yourdomain.com
...
</VirtualHost>Enable remoteip so Apache logs the real client IP from X-Forwarded-For:
sudo a2enmod remoteipAdd to your vhost:
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1Restart Apache:
sudo systemctl restart apache2For a Nginx origin
Change the listen directive on your origin vhost to listen 127.0.0.1:8080; and reload Nginx. If you are running WordPress, update WP_HOME and WP_SITEURL to use https:// since Nginx terminates TLS upstream.
Step 8: Verify the Cache is Working
Send two requests to your site and inspect the headers:
curl -sI https://yourdomain.com/ | grep -Ei 'x-cache|age|cache-control'The first response should be a MISS:
x-cache: MISS
cache-control: max-age=3600The second response should be a HIT:
x-cache: HIT
x-cache-hits: 1
age: 4
cache-control: max-age=3600If you see X-Cache: MISS on every request, jump to the Troubleshooting section -- something in the request (usually a cookie) is forcing a pass.
Post-Install: Purging, Monitoring, and Tuning
Purging a specific URL
With the ACL we defined in VCL, PURGE from the local host is allowed:
curl -X PURGE http://localhost:6081/path/to/article/WordPress plugins like W3 Total Cache and WP Rocket can be pointed at http://localhost:6081 so that editing a post purges the relevant entries automatically.
Banning a pattern
PURGE removes one URL. To invalidate everything matching a pattern, use varnishadm with a ban expression:
sudo varnishadm ban 'req.url ~ "^/blog/"'This marks every cached object whose URL starts with /blog/ as invalid. On the next request for a matching URL, Varnish fetches a fresh copy from the backend.
Real-time traffic inspection with varnishlog
sudo varnishlogThis streams every transaction through the cache -- headers in, headers out, which VCL subroutine fired, how long the backend took. To filter for cache misses only:
sudo varnishlog -q "VCL_call eq 'MISS'"To watch a single client IP:
sudo varnishlog -q "ReqHeader:X-Forwarded-For eq '203.0.113.10'"Statistics with varnishstat
sudo varnishstatPress h for help. The two counters you care about most are:
- MAIN.cache_hit -- total cache hits
- MAIN.cache_miss -- total cache misses
Admin console with varnishadm
sudo varnishadmFrom the prompt you can list VCL versions, load a new one without a restart, inspect live config, and view the backend health:
varnish> vcl.list
varnish> backend.list
varnish> ban.listTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
varnishd[...]: Error: VCL compilation failed on restart | Syntax error in default.vcl | Run sudo varnishd -C -f /etc/varnish/default.vcl to see the line number. Fix and sudo varnishreload. |
X-Cache: MISS on every request | A cookie or request header is forcing a pass | Run sudo varnishlog -q "VCL_call eq 'PASS'" and look at ReqHeader:Cookie. Add any missing tracking cookies to the strip regex in vcl_recv. |
| Logged-in users see other users' content | Logged-in detection is not matching | Confirm the WordPress login cookie prefix (wordpress_logged_in_) in vcl_recv. For WooCommerce add woocommerce_cart_hash and woocommerce_items_in_cart. |
503 Backend fetch failed | Origin on :8080 is down or unreachable | Check sudo systemctl status apache2 (or nginx). Test directly: curl -v http://127.0.0.1:8080. Check firewall: sudo ufw status. |
| Cache is being bypassed by query strings | Tracking parameters (utm_*, fbclid) creating unique cache keys | Normalise query strings in vcl_recv: set req.url = regsuball(req.url, "\?(utm_[^&]+</td><td>fbclid=[^&]+)&?", "?"); |
| Varnish restarts clear the cache | malloc storage is RAM-only and non-persistent | This is expected. Use -s file,/var/lib/varnish/cache.bin,20g if you need survival across restarts. |
Child (XXXX) died signal=9 (OOM kill) | malloc size exceeds available RAM | Reduce -s malloc,Ng or upgrade RAM. Leave at least 2 GB for the OS and backend. |
| PURGE returns 403 | Client IP is not in the purge ACL | Add the IP to the acl purge {} block in VCL and reload. |
Viewing Varnish logs
sudo journalctl -u varnish -fVarnish's most useful log is the shared-memory log exposed via varnishlog, not systemd. The systemd journal mostly shows startup errors and VCL compile failures.
FAQ
How much RAM should I give Varnish?
Give Varnish enough memory to hold your working set -- the subset of pages that get hit frequently. For a typical WordPress site with 1,000 cacheable pages averaging 50 KB each, that is 50 MB of actual content. Size -s malloc at 5-10x that because Varnish also stores headers, revalidation state, and some overhead per object. On a 12 GB VPS, a 2 GB malloc cache is a reasonable default. Always leave at least 2 GB free for the OS and your backend.
Can I run Varnish on the same server as my WordPress site?
Yes, and that is the most common deployment. The three-tier pipeline (Nginx TLS → Varnish → Apache/PHP-FPM on :8080) all lives on one VPS. Our CloudCore Professional plan is right-sized for this: 12 GB RAM lets you run a 2 GB Varnish cache, a PHP-FPM pool with 8-12 workers, and MySQL comfortably.
Does Varnish handle HTTPS?
Not natively. Varnish is strictly HTTP. Use Nginx (or HAProxy, or Caddy) in front for TLS termination. This is an intentional design choice -- Varnish focuses on caching and lets specialised tools handle encryption.
How do I cache logged-in WordPress users?
You usually should not -- the whole point of logged-in pages is that they are personalised. If you really need to (for example, a dashboard that shows the same data to everyone in the "subscriber" role), use Vary: Cookie from the backend and key on a specific cookie. This is advanced VCL and out of scope for a starter guide. For most sites, serve anonymous visitors from cache and let logged-in users hit the backend.
How do I purge the whole cache?
sudo varnishadm ban 'req.url ~ "."'This bans every URL. Varnish will re-fetch each one on its next request. For a full flush, restart the service: sudo systemctl restart varnish.
Can I use Varnish with Cloudflare in front?
Yes. Cloudflare terminates TLS at its edge, forwards to your Nginx on :443, which forwards to Varnish on :6081, which forwards to the origin on :8080. Two layers of cache with different roles: Cloudflare caches globally across data centres, Varnish caches at your origin. Make sure Nginx's real_ip_header is set to CF-Connecting-IP so Varnish and your backend see the true client IP.
Next Steps
Now that Varnish is sitting between Nginx and your origin, consider these follow-ups:
- Tune WordPress cache plugins -- Install W3 Total Cache or WP Rocket and configure it to call
PURGE http://localhost:6081/...when posts are updated. See our WordPress install guide for the baseline setup. - Add monitoring -- Ship
varnishstatcounters into Prometheus using thevarnish_exporterand chart hit ratio, MAIN.n_lru_nuked, and backend latency. - Experiment with ESI -- Varnish supports Edge Side Includes. You can cache most of a page for hours while leaving a single fragment (like a personalised greeting) to be assembled per-request.
- Try the file storage backend -- Switch
-s malloc,1gto-s file,/var/lib/varnish/cache.bin,20gif your cacheable content is larger than RAM. - Read the official docs -- The Varnish Book and varnish-cache.org documentation are excellent and cover advanced topics like VMODs, probes, directors, and load balancing.
Make Varnish Easier with the Right VPS>
Varnish thrives on RAM, fast single-core performance, and NVMe disk. Our CloudCore Professional plan gives you 6 vCPU, 12 GB RAM, and 100 GB NVMe -- exactly the profile Varnish loves.>
- Full root access for systemd and VCL tuning
- Deploy in under 60 seconds
- EUR 19.99/month, unmetered bandwidth>
Launch a CloudCore Professional VPS and start accelerating your site today.