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. - 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 proofsThe 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 |
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 — assertcookie=none,X-Cache: MISSthenHIT. - Non-static request with
Cookieis passed per-client — assert separateX-Backend-Request-Idvalues; bob must not see alice's response. - Response with
Set-Cookieis never shared from cache — assert separate request ids andSet-Cookievalues per client.
Out of scope: additional routes, extra Cache-Control permutations, replacing the nginx smoke backend.
(c) 2017 Jonathan Baldie