Varnish is basically Redis for HTTP requests. Instead of serving identical HTTP requests and eating up resources, it caches them for as long as you like. Put it in front of your HTTP server and it intercepts everything — one of the simplest ways to make a website faster.
Pull the image:
docker pull jonbaldie/varnishOr clone the repo and build locally:
make| Debian Version | Varnish Version |
|---|---|
| bookworm | 7.1.1 |
Expose Varnish on port 80 and point it at your web server via a vcl config file. See the Varnish docs for the full picture.
The standalone image embeds /etc/varnish/default.vcl and generates /etc/varnish/backend.vcl. Docker Compose uses the same backend configuration adapter and mounts only the shared cache policy.
The container starts varnishd from named runtime values instead of a single baked command string.
VARNISH_LISTENdefaults to0.0.0.0:80VARNISH_VCLdefaults to/etc/varnish/default.vclVARNISH_STORAGEdefaults tomalloc,1gVARNISH_EXTRA_ARGSappends extravarnishdflags when you need a small escape hatch
For advanced cases, VARNISH_START still works as a full-command override, but it cannot be combined with the narrower VARNISH_* settings.
Backend configuration also has named values:
VARNISH_BACKEND_HOSTdefaults to127.0.0.1VARNISH_BACKEND_PORTdefaults to8080VARNISH_BACKEND_PROBE_PATHdefaults to/
These render the embedded /etc/varnish/backend.vcl. If you supply a custom VARNISH_VCL, keep backend wiring in that VCL instead.
This image runs varnishd as the container process, not as a distro service.
Do not run sudo service varnish restart inside the container.
Restart the container instead:
docker restart <container-name>With the sample Compose file, restart the Varnish service:
docker compose restart varnishIf you changed mounted cache policy or backend environment values, restart the container so varnishd starts
again with the updated configuration. If you changed image files, rebuild first
with docker compose up -d --build.
The embedded default VCL includes generated backend configuration and the shared cache policy. The repo default.vcl remains a standalone sample for custom VCL mounts. The shared cache policy includes:
- Backend health probe — 2s timeout, 5s interval, sliding window of 5 checks, threshold of 3. Override the probe path with
VARNISH_BACKEND_PROBE_PATH. - Host header compliance — rejects HTTP/1.1 requests with a missing, empty, whitespace-only, or invalid
Hostfield value with 400 Bad Request (RFC 9112 §7.1, RFC 9110 §7.2); normalises validHostheader casing to lowercase. - Authorization header compliance — passes requests carrying an
Authorizationheader directly to origin, preventing authenticated responses from being cached or leaked across clients (RFC 9111 §3.5). - Vary header compliance — marks origin responses containing wildcard
Vary: *as uncacheable hit-for-miss, preventing shared caching (RFC 9111 §4.1). - Zero-freshness static assets — static-extension URLs only receive the policy TTL when the origin granted positive freshness; responses with zero freshness (
max-age=0,s-maxage=0, or anExpiresdate in the past) stay hit-for-miss (RFC 9111 §5.2). AnExpiresvalue that is not a valid HTTP-date — the common0,-1, or any unparseable string — counts as already expired too (RFC 9111 §5.3) and stays hit-for-miss for every URL, static or not, unlessCache-Controlsuppliesmax-age/s-maxage, which overridesExpires. - PURGE ACL — allows cache purging from
localhostand127.0.0.1. - Accept-Encoding normalisation — normalises to
gzipordeflatefor text; unsets for binary assets. - Cookie stripping — removes cookies for static assets to improve cache hit rates.
- Cache TTLs — 1 day TTL with 7 day grace for static assets; 1 hour grace for other content.
- 500 error handling — sets TTL to 0 and 24h grace for backend 5xx errors.
- X-Cache header — adds
HIT/MISSfor cache observability.
The backend address differs between modes through the adapter: Docker Compose uses web:80; standalone defaults to 127.0.0.1:8080. render-vcl generates backend VCL from VARNISH_BACKEND_HOST, VARNISH_BACKEND_PORT, and VARNISH_BACKEND_PROBE_PATH.
The repo includes a docker-compose.yml sample:
varnishservice on port 80, usingVARNISH_BACKEND_*to target the backend.webbackend runningnginx:alpine.
docker compose upVarnish doesn't understand HTTPS natively, so requests need rerouting:
HTTPS → Nginx:443 → Varnish:80 → Nginx:80 (internal) → Website
HTTP → Varnish:80 → Nginx:80 (internal) → Website
With Docker networking this is straightforward to set up and performs well.
make test # default build, smoke, integration, and cache checks
make test-e2e-hard # stronger hostile-backend proofsFleet shares an 8-core macOS host with other repositories. During iteration, run one named test target at a time, for example make test-vcl-compile. Do not start independent Docker test targets in parallel.
Before handoff, run the complete Docker gates serially:
COMPOSE_PARALLEL_LIMIT=1 make -j1 test test-e2e-hardWait for this command to return before you start another Docker test or campaign. The test recipes use teardown traps to remove containers and Compose resources. Do not interrupt or overlap that teardown.
The nginx:alpine compose backend covers the happy-path smoke tests. A separate hostile backend fixture exists only to prove behaviours nginx can't expose clearly enough for E2E assertions.
Endpoints required:
| Endpoint | Purpose |
|---|---|
GET /static/app.css |
Cacheable static asset; must echo whether a Cookie reached origin |
GET /account |
Dynamic pass-through; must echo caller identity from Cookie |
GET /set-cookie |
Must return Set-Cookie; must not be shared across clients |
GET /location-target, GET /content-location-target |
Cacheable targets referenced by unsafe-method responses (RFC 9111 §4.4) |
unsafe /create-rel, /create-abs, /create-abs-mixed-host, /create-abs-upper-host, /create-cross-host, /create-cross-host-mixed, /create-content-location, /create-content-location-abs-mixed |
Must return Location/Content-Location referencing the targets above, including mixed-case and uppercase same-host and cross-host forms |
Every response must include:
X-Backend: hostile— proves the backend handled the request.X-Backend-Request-Id— unique per origin hit, used to detect cache reuse.
Response contract:
/static/app.css— body containsasset=app.cssandcookie=nonewhen no cookie reached origin. Should be cacheable; tests expectX-Cache: MISSthenX-Cache: HIT./account— body containsroute=accountandclient=<name>from the cookie. Must not carrySet-Cookie. Both requests should produceX-Cache: MISSwith different request ids./set-cookie— body containsroute=set-cookieandclient=<name>. Must includeSet-Cookie: session=<name>; Path=/. Every request should produceX-Cache: MISSwith different request ids across clients.
Proof matrix:
- Static asset with
Cookiestrips the cookie before origin on safe GET/HEAD requests — assertcookie=none,X-Cache: MISSthenHIT. Mutating requests (POST,PUT,DELETE,PATCH) preserveCookieheaders and pass directly to origin. - Non-static request with
Cookieis passed per-client — assert separateX-Backend-Request-Idvalues; bob must not see alice's response. - Dynamic URL whose query value ends in a static extension (e.g.
?q=jquery.js) is not treated as a static asset — cookies reach origin per-client with separate request ids, while a real static asset with a query string (/static/app.css?v=2) still strips cookies and caches. - Response with
Set-Cookieis never shared from cache — assert separate request ids andSet-Cookievalues per client. - A successful unsafe request whose response carries
LocationorContent-Location(RFC 9111 §4.4) invalidates the cached object for the referenced URI on the same host (relative or absolute reference), while a cross-host reference must not invalidate same-host cache entries.
Out of scope: additional routes, extra Cache-Control permutations, replacing the nginx smoke backend.
(c) 2017 Jonathan Baldie