--- title: "How to Configure Varnish Cache with Nginx on Ubuntu 24.04" description: "Install Varnish Cache on Ubuntu 24.04 in front of Nginx, write a VCL with health checks, cookie rules and purging, and verify cache hits with varnishstat." category: "Web Servers" language: en source: https://cubepath.com/docs/web-servers/varnish-cache-configuration updated: 2026-09-25 --- # How to Configure Varnish Cache with Nginx on Ubuntu 24.04 Varnish Cache is an HTTP reverse proxy that stores responses in memory and serves repeated requests without touching your application. For pages that are the same for every visitor, it can cut response times to a few milliseconds and remove most of the load from the backend. In this tutorial you will install Varnish on Ubuntu 24.04, put it on port 80 in front of Nginx, write a VCL configuration with a health check, cookie handling and cache purging, verify that responses are served from cache, and add HTTPS with Nginx in front of Varnish. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has `sudo` privileges. - At least 1 GB of RAM. Varnish keeps its cache in memory, and you'll assign it a fixed amount. - Nginx installed and serving your site on port 80. This guide assumes the default document root `/var/www/html`. - A domain name pointing to the server if you want to follow the HTTPS step. This guide uses `your_domain` as a placeholder. The final layout looks like this: | Component | Listens on | Role | |---|---|---| | Nginx (TLS) | `:443` | Terminates HTTPS and forwards to Varnish | | Varnish | `:80` | Caches responses, forwards misses to the backend | | Nginx (backend) | `127.0.0.1:8080` | Serves the site or proxies to your application | ## Step 1 - Installing Varnish Varnish is available in the Ubuntu 24.04 repositories: ```bash sudo apt update sudo apt install varnish ``` Check the installed version: ```bash varnishd -V ``` ```output varnishd (varnish-7.1.1 revision ...) Copyright (c) 2006 Verdens Gang AS Copyright (c) 2006-2022 Varnish Software ``` The package starts Varnish on port 6081 with the configuration in `/etc/varnish/default.vcl`. You'll move it to port 80 in Step 3, after freeing that port. ## Step 2 - Moving Nginx to port 8080 Varnish needs port 80, so Nginx must listen somewhere else. Bind it to `127.0.0.1:8080` so the backend can only be reached through Varnish. Open the default site: ```bash sudo nano /etc/nginx/sites-available/default ``` Change the two `listen` lines at the top of the `server` block: ```nginx server { listen 127.0.0.1:8080 default_server; root /var/www/html; index index.html index.htm index.nginx-debian.html; server_name _; location / { try_files $uri $uri/ =404; } } ``` Remove the `listen [::]:80 default_server;` line, and change `listen 80` in any other server block under `/etc/nginx/sites-enabled/` the same way. Varnish connects from `127.0.0.1`, so Nginx logs would show that address for every request. Varnish adds the client's IP to the `X-Forwarded-For` header, and the `realip` module in Ubuntu's Nginx can restore it. `real_ip_recursive on` skips trusted local addresses in the header, which matters once Nginx also terminates HTTPS in front of Varnish (Step 7). Create this file: ```bash sudo nano /etc/nginx/conf.d/real-ip.conf ``` ```nginx set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; real_ip_recursive on; ``` Test the configuration and restart Nginx (a restart is needed to release port 80): ```bash sudo nginx -t sudo systemctl restart nginx ``` Check that Nginx answers on the new port: ```bash curl -sI http://127.0.0.1:8080/ | head -n 1 ``` ```output HTTP/1.1 200 OK ``` Your site is offline on port 80 until you finish Step 3. ## Step 3 - Running Varnish on port 80 The listening port and cache size are set on the `varnishd` command line in the systemd unit. Look at the unit shipped by the package: ```bash systemctl cat varnish ``` The `ExecStart` line looks similar to this: ```ini ExecStart=/usr/sbin/varnishd \ -j unix,user=vcache \ -F \ -a :6081 \ -T localhost:6082 \ -f /etc/varnish/default.vcl \ -S /etc/varnish/secret \ -s malloc,256m ``` Don't edit that file, because package upgrades overwrite it. Create a drop-in override instead: ```bash sudo systemctl edit varnish ``` In the editor, add the following between the comment lines. Copy the `ExecStart` from your own output and change only the `-a` and `-s` values. The empty `ExecStart=` line is required: it clears the original command before setting the new one. ```ini [Service] ExecStart= ExecStart=/usr/sbin/varnishd \ -j unix,user=vcache \ -F \ -a :80 \ -T localhost:6082 \ -f /etc/varnish/default.vcl \ -S /etc/varnish/secret \ -s malloc,512m ``` - `-a :80` makes Varnish listen on port 80 on all addresses. - `-s malloc,512m` sets the cache size. Choose a value that leaves enough RAM for Nginx, your application and the operating system; Varnish uses somewhat more memory than this value for its own overhead. - `-T localhost:6082` is the management interface used by `varnishadm`. Keep it on localhost. Save and close the file. Don't restart Varnish yet; you'll load the new unit together with the new VCL in the next step. ## Step 4 - Writing the VCL configuration VCL (Varnish Configuration Language) decides what is cached and for how long. Varnish's built-in logic is already safe: it doesn't cache requests with a `Cookie` or `Authorization` header or responses with `Set-Cookie`, and it caches other `200` responses for 2 minutes unless the backend sends `Cache-Control` headers. Your VCL runs first and only needs to adjust that behavior. Back up the default file and open it: ```bash sudo cp /etc/varnish/default.vcl /etc/varnish/default.vcl.orig sudo nano /etc/varnish/default.vcl ``` Replace its contents with the following: ```vcl vcl 4.1; backend default { .host = "127.0.0.1"; .port = "8080"; .connect_timeout = 5s; .first_byte_timeout = 60s; .probe = { .url = "/"; .timeout = 2s; .interval = 5s; .window = 5; .threshold = 3; } } acl purge { "localhost"; "127.0.0.1"; "::1"; } sub vcl_recv { # Allow cache invalidation from the server itself if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(405, "Not allowed")); } return (purge); } # Never cache Let's Encrypt challenges or admin areas if (req.url ~ "^/\.well-known/acme-challenge/" || req.url ~ "^/admin") { return (pass); } # Static files are the same for everyone: ignore cookies if (req.url ~ "\.(css|js|png|jpe?g|gif|svg|webp|avif|ico|woff2?)(\?.*)?$") { unset req.http.Cookie; } } sub vcl_backend_response { # Cache static files for a day and drop any cookies the backend sets on them if (bereq.url ~ "\.(css|js|png|jpe?g|gif|svg|webp|avif|ico|woff2?)(\?.*)?$") { unset beresp.http.Set-Cookie; set beresp.ttl = 1d; } # Keep expired objects for 6 hours to serve them if the backend is down set beresp.grace = 6h; } sub vcl_deliver { if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } } ``` What this configuration does: - **Backend and probe.** Varnish checks `/` every 5 seconds and considers the backend healthy when at least 3 of the last 5 checks succeed. - **Purge.** A `PURGE` request from the server removes that URL from the cache. Requests from other addresses get a `405`. - **Pass.** Paths that must always reach the backend skip the cache. Add your own, such as `/wp-admin` or `/cart`, to this condition. - **Static files.** Removing cookies lets Varnish cache images, CSS and JavaScript even when the site sets tracking or session cookies. - **Grace.** If the backend is down or slow, Varnish serves the expired copy for up to 6 hours while it tries to fetch a fresh one. - **X-Cache header.** Shows whether a response was a hit or a miss, which you'll use to verify the setup. Check that the VCL compiles before loading it: ```bash sudo varnishd -C -f /etc/varnish/default.vcl > /dev/null && echo "VCL OK" ``` ```output VCL OK ``` If there's a syntax error, `varnishd` prints the file, line and column of the problem instead. Now apply the new unit and VCL: ```bash sudo systemctl daemon-reload sudo systemctl restart varnish ``` Confirm that Varnish listens on port 80: ```bash sudo ss -tlnp | grep varnishd ``` ```output LISTEN 0 1024 0.0.0.0:80 0.0.0.0:* users:(("cache-main",pid=4321,fd=6)) LISTEN 0 1024 [::]:80 [::]:* users:(("cache-main",pid=4321,fd=7)) LISTEN 0 10 127.0.0.1:6082 0.0.0.0:* users:(("varnishd",pid=4310,fd=9)) ``` If UFW is active and you haven't opened HTTP yet, allow it with `sudo ufw allow 80/tcp`. ## Step 5 - Verifying that responses are cached Request the same page twice and compare the headers: ```bash curl -sI http://your_server_ip/ | grep -E "X-Cache|Age|Via" curl -sI http://your_server_ip/ | grep -E "X-Cache|Age|Via" ``` ```output X-Cache: MISS Age: 0 Via: 1.1 your-hostname (Varnish/7.1) X-Cache: HIT Age: 3 Via: 1.1 your-hostname (Varnish/7.1) ``` The first request went to Nginx, the second came from memory. `Age` shows how many seconds the object has been in the cache. Check the health of the backend: ```bash sudo varnishadm backend.list ``` ```output Backend name Admin Probe Health Last change boot.default probe 5/5 healthy Thu, 25 Sep 2026 10:05:00 GMT ``` Look at the hit and miss counters: ```bash varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss ``` ```output MAIN.cache_hit 1 0.00 Cache hits MAIN.cache_miss 1 0.00 Cache misses ``` On a live site, a good hit ratio depends on your content, but if `cache_miss` grows as fast as `cache_hit`, find out why pages aren't cached. `varnishlog` shows every step of a request. This example follows requests for `/`: ```bash sudo varnishlog -g request -q 'ReqURL eq "/"' ``` Look for `VCL_call` and `VCL_return` lines: `pass` or `hit-for-pass` tells you Varnish decided not to cache, and the request headers above them (usually `Cookie`) or the `Set-Cookie` and `Cache-Control` response headers show the reason. Press `Ctrl+C` to stop. ## Step 6 - Purging and reloading After you update a page, remove it from the cache with the `PURGE` method from the server: ```bash curl -X PURGE http://127.0.0.1/ ``` The response is a small HTML page with `200 Purged`. The next request for `/` is a miss and fetches the new content. When you change the VCL later, you don't need a restart, which would empty the cache. Compile the file as in Step 4, then reload: ```bash sudo systemctl reload varnish ``` The reload loads the new VCL into the running Varnish and keeps the cached objects. ## Step 7 - Adding HTTPS in front of Varnish Varnish doesn't handle TLS. The usual solution is an Nginx server block on port 443 that terminates HTTPS and forwards requests to Varnish on port 80. First get a certificate. Because Varnish passes `/.well-known/acme-challenge/` to Nginx on port 8080, which serves `/var/www/html`, the Certbot webroot method works: ```bash sudo apt install certbot sudo certbot certonly --webroot -w /var/www/html -d your_domain -d www.your_domain ``` Create the TLS server block: ```bash sudo nano /etc/nginx/sites-available/your_domain-tls ``` ```nginx server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name your_domain www.your_domain; ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://127.0.0.1:80; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto https; } } ``` Enable it, open port 443 and reload Nginx: ```bash sudo ln -s /etc/nginx/sites-available/your_domain-tls /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx sudo ufw allow 443/tcp ``` Certbot's renewal timer reuses the webroot method. Add a deploy hook so Nginx loads renewed certificates: ```bash sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail systemctl reload nginx ``` ```bash sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh sudo certbot renew --dry-run ``` Test HTTPS with `curl -sI https://your_domain/ | grep -E "HTTP/|X-Cache"`. Two consecutive requests should show `X-Cache: MISS` and then `HIT`. > Important: If your application behaves differently for HTTP and HTTPS (for example, it redirects to HTTPS based on `X-Forwarded-Proto`), add `hash_data(req.http.X-Forwarded-Proto);` in a `vcl_hash` subroutine so Varnish caches the two variants separately. Otherwise a cached redirect can be served to HTTPS visitors and cause a redirect loop. ## Troubleshooting **Varnish fails to start with "Could not get socket :80: Address already in use".** Nginx or Apache is still listening on port 80. Find it with `sudo ss -tlnp | grep ':80 '`, fix its `listen` directives and restart it. **Every response is `X-Cache: MISS`.** The request carries cookies or the backend sends `Set-Cookie`, `Cache-Control: private`, `no-cache` or `max-age=0`. Check the headers with `curl -sI http://127.0.0.1:8080/` and with `varnishlog`, then remove unneeded cookies in `vcl_recv` or fix the headers in the application. **`503 Backend fetch failed`.** Varnish can't reach Nginx on `127.0.0.1:8080`, or the probe marks it as sick. Run `sudo varnishadm backend.list` and `curl -I http://127.0.0.1:8080/`. If the probe URL returns a redirect or an error, point `.url` to a path that returns `200`. **Logged-in users see each other's pages.** A page with personal content was cached. Make sure those URLs, or requests with the session cookie, reach `return (pass)` in `vcl_recv`, then purge the affected URLs. **Memory usage grows beyond the `malloc` value.** That size covers cached objects only; each thread and object also has overhead. Lower the `-s malloc` value if the server starts swapping. ## Conclusion Varnish now serves your site on port 80 from memory, forwards misses to Nginx on port 8080, keeps serving stale content if the backend goes down, and sits behind Nginx for HTTPS. Watch `varnishstat` for a few days to see how much traffic is served from cache, and extend the `pass` and cookie rules as you learn how your application behaves. As next steps, set proper `Cache-Control` headers in your application, add purge calls to your deployment or CMS, and monitor the backend health from your monitoring system.