Optimization

Configure Varnish Cache in Front of a Web Server on Ubuntu 24.04

The first thing you notice when your web server starts buckling under traffic is the latency. Every request hits PHP, reads from the database, and builds a page. That’s expensive. Varnish Cache sits in front of your web server, serves cached copies of pages and API responses from memory, and drops the load on your back end by orders of magnitude. A properly tuned Varnish can serve thousands of requests per second from a single VPS without ever touching Nginx or Apache. This guide walks through installing Varnish 7.x on Ubuntu 24.04, configuring it as a reverse proxy, tuning the memory allocation, and verifying that caching actually works. You will need a VPS with at least 2 GB of RAM, Varnish needs room to hold your hot objects in memory.

Prerequisites

  • A VPS running Ubuntu 24.04 LTS with a non-root sudo user.
  • A web server (Nginx or Apache) already installed and serving content on port 80 or 8080.
  • Root or sudo access to install packages and modify systemd service files.
  • At least 2 GB of RAM allocated to the VPS; more is better for cache storage.
  • If you need a VPS with dedicated IPv4 and NVMe storage, consider a Linux VPS with enough headroom for caching workloads.

Why Put Varnish in Front of Your Web Server?

Modern web servers are fast at serving static files, but they are not built to hold a large number of objects in memory for sub-millisecond delivery. Varnish is. It sits on port 80 or 443, receives the HTTP request, checks its in-memory hash table for a cached copy, and, if it finds one, returns it immediately without handing the request downstream. This means PHP-FPM, Apache’s mod_php, or any application process stays idle while Varnish does the heavy lifting. For a WordPress site or a read-heavy API, the difference in TTFB (time to first byte) is noticeable: from 200, 500 ms down to 1, 5 ms for cached pages. Varnish also handles concurrent connections more efficiently than most web servers because it is event-driven and single-threaded per request.

Step 1, Install Varnish on Ubuntu 24.04

Ubuntu 24.04 ships with Varnish 7.x in the Universe repository. Install it directly:

sudo apt update
sudo apt install varnish -y

Verify the installation:

varnishd -V

Expected output (minor version may differ):

varnishd (varnish-7.5.0 revision ...)
Copyright (c) 2006 Verdens Gang AS
Copyright (c) 2006-2023 Varnish Software

The default installation places the main configuration at /etc/varnish/default.vcl and the systemd service at /lib/systemd/system/varnish.service. Do not modify the service file directly, use systemctl edit varnish instead, which places an override in /etc/systemd/system/varnish.service.d/.

Important: By default, Varnish listens on port 6081 and the systemd unit binds to that port. You will change this in the next step.

Step 2, Configure Varnish to Listen on Port 80

Your web server currently occupies port 80. Move your web server to a different port (e.g., 8080) and put Varnish on port 80. This is the standard architecture: client → Varnish:80 → backend:8080.

First, change your web server’s listening port. For Nginx, edit /etc/nginx/sites-available/default or your site’s config and change:

listen 8080;
listen [::]:8080;

For Apache, edit /etc/apache2/ports.conf and the virtual host file:

Listen 8080
<VirtualHost *:8080>
...
</VirtualHost>

Then reload the web server:

sudo systemctl reload nginx   # or apache2

Now tell Varnish to listen on port 80. Use systemctl edit varnish to override the service:

sudo systemctl edit varnish

This opens a blank file. Paste the following:

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

The empty ExecStart= line clears the default definition. The new line runs Varnish with three important arguments:

  • -a :80, bind to port 80 on all interfaces.
  • -f /etc/varnish/default.vcl, the VCL configuration file.
  • -s malloc,1G, allocate 1 GB of RAM as cache storage. Adjust this to fit your VPS. On a 4 GB VPS, use malloc,2G; on 2 GB, start with malloc,768M. Never allocate more than 50, 60% of total RAM to Varnish, or the OS might OOM-kill it under load.

Save and close the file. Reload the systemd daemon and restart Varnish:

sudo systemctl daemon-reload
sudo systemctl restart varnish

Verify the new port:

sudo ss -tulpn | grep varnishd

You should see Varnish listening on port 80. Your web server should be on port 8080.

Step 3, Write a Minimal VCL Configuration

VCL (Varnish Configuration Language) is a domain-specific language that defines how Varnish should handle requests: what to cache, how long to keep it, and what to bypass. A minimal VCL starts with a backend definition pointing to your web server on port 8080.

Edit /etc/varnish/default.vcl:

sudo nano /etc/varnish/default.vcl

Replace the contents with:

vcl 4.1;

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

sub vcl_recv {
    # Normalize the Host header
    if (req.http.host ~ "^www\.") {
        set req.http.host = regsub(req.http.host, "^www\.", "");
        return (synth(301, "Moved Permanently"));
    }

    # Bypass cache for non-GET/HEAD requests
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Bypass cache for logged-in users (WordPress example)
    if (req.http.cookie ~ "wordpress_logged_in" || req.http.cookie ~ "comment_author") {
        return (pass);
    }

    # Bypass admin and login paths
    if (req.url ~ "^/wp-admin|^/wp-login|^/admin|^/login|^/cart|^/checkout") {
        return (pass);
    }

    # Remove cookies for static files to allow caching
    if (req.url ~ "\.(css|js|png|gif|jpg|jpeg|ico|svg|woff2?|ttf|eot)$") {
        unset req.http.cookie;
    }

    return (hash);
}

sub vcl_backend_response {
    # Cache static files for 30 days
    if (bereq.url ~ "\.(css|js|png|gif|jpg|jpeg|ico|svg|woff2?|ttf|eot)$") {
        set beresp.ttl = 30d;
        unset beresp.http.set-cookie;
    }

    # Default TTL
    set beresp.ttl = 4h;

    # Do not cache if backend returns error
    if (beresp.status >= 400) {
        set beresp.ttl = 0s;
        return (deliver);
    }

    # Do not cache if the backend sets a cookie
    if (beresp.http.set-cookie) {
        set beresp.ttl = 0s;
    }
}

sub vcl_deliver {
    # Add a header to indicate cache hit/miss
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
    unset resp.http.Via;
    unset resp.http.X-Varnish;
}

This configuration:

  • Normalizes the www prefix with a 301 redirect.
  • Bypasses non-GET requests, logged-in sessions, admin paths, and e-commerce checkout pages.
  • Strips cookies from static file requests so they can be cached.
  • Sets TTL per content type: static assets for 30 days, regular pages for 4 hours.
  • Does not cache backend errors or responses that set cookies.
  • Adds an X-Cache header so you can see whether a response came from cache (HIT) or your web server (MISS).

Apply the configuration:

sudo systemctl restart varnish

Test with curl:

curl -I http://your-server-ip/

Look for the X-Cache header in the response:

HTTP/1.1 200 OK
...
X-Cache: MISS

Run the same request again, you should see X-Cache: HIT:

curl -I http://your-server-ip/
...
X-Cache: HIT

Step 4, Tune Varnish Memory and Parameters

The storage backend malloc is the simplest option. Varnish uses a slab allocator similar to how a database manages its buffer pool. A few tuning knobs can improve cache efficiency on a VPS with limited RAM:

ParameterDescriptionRecommended Value
-s malloc,NCache size in RAM50, 60% of total RAM (e.g., 2.5 GB on a 4 GB VPS)
-p thread_pool_maxMax worker threads500, 1000
-p thread_pool_add_delayDelay (ms) in creating new threads10
-p default_ttlDefault TTL for objects without explicit TTL120 (seconds)
-p feature=+http2Enable HTTP/2(optional, requires a TLS terminator like Hitch or Nginx)

To add these parameters, edit the systemd override again:

sudo systemctl edit varnish

Append the -p flags to the ExecStart line:

ExecStart=/usr/sbin/varnishd -a :80 -f /etc/varnish/default.vcl -s malloc,2G -p thread_pool_max=500 -p thread_pool_add_delay=10 -p default_ttl=120

Reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart varnish

Caution: Setting thread_pool_max too high (e.g., 5000) on a low-RAM VPS can cause thread contention and increased memory usage. Start conservative and increase under load testing.

Step 5, Enable Varnish Logging

Varnish ships with two logging tools: varnishlog (verbose, every request) and varnishstat (aggregated counters). Both are essential for tuning.

Check the hit rate with varnishstat:

varnishstat -1 | grep -E "(MAIN.cache_hit|MAIN.cache_miss|MAIN.sess_conn)"

Output example:

MAIN.cache_hit           1243         0.00         4.22 ...
MAIN.cache_miss           121         0.00         0.41 ...
MAIN.sess_conn             25         0.00         0.08 ...

A hit rate above 80% is healthy. If you see many misses, review your VCL rules, you may be bypassing cache unnecessarily.

For real-time logging of individual requests, use varnishlog with a filter:

varnishlog -g request -q "ReqURL ~ '^/'"

This prints every request in real time. Press Ctrl+C to stop.

Troubleshooting

502 Bad Gateway from Varnish

This means Varnish cannot connect to the backend. Verify the backend is running on port 8080:

curl -I http://127.0.0.1:8080/
systemctl status nginx

If the backend is fine, check default.vcl for a correct .host and .port.

Varnish Not Starting After Service Edit

A typo in the ExecStart line breaks the unit. Check the error:

sudo journalctl -u varnish -n 50 --no-pager

Common mistakes: missing hyphen before -a, unbalanced quotes, or a wrong path for the VCL file.

X-Cache Header Always Shows MISS

This usually indicates the request is being passed through. Run varnishlog while making a request:

varnishlog -g request -q "ReqURL ~ '/' "

Look for lines containing Pass or FetchError. Common causes: cookies on the request that do not match the VCL bypass rules, or the backend returning Set-Cookie on every response.

FAQ

Does Varnish support HTTPS?

Varnish does not terminate TLS natively. You need a TLS proxy in front of it, such as Hitch (developed by the same team) or Nginx on port 443 that forwards decrypted traffic to Varnish on port 80. A common stack is: client → Nginx (TLS):443 → Varnish:80 → backend:8080.

How much RAM should I allocate to Varnish?

Start with 50% of your VPS RAM. On a 2 GB VPS, 1 GB is a safe starting point. Monitor varnishstat for the n_lru_nuked counter, if it grows fast, you are running out of cache space and need more RAM or a smaller working set.

Can I use Varnish with WordPress?

Yes, but you need to handle cookies correctly. The example VCL in this guide bypasses cache for logged-in users and admin pages. For optimal performance, install a cache-purge plugin like Varnish HTTP Purge or WP Rocket that sends PURGE requests when content changes.

How do I clear the entire Varnish cache?

varnishadm ban req.url ".*"

This invalidates all objects. You can also ban by host header: varnishadm ban req.http.host == "example.com". Banning is a soft invalidation, the objects are discarded on the next request, not immediately freed from RAM.

Does Varnish work with CDNs?

Yes. If you use a CDN like Cloudflare, the CDN’s edge nodes cache your content first. Varnish acts as a second-layer cache behind the CDN, reducing load on your origin server. Set the backend’s TTL appropriately so the CDN does not serve stale content.

Related articles

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.