component-varnish
A Varnish full-page cache in front of the app, for sites built on oym/craft-cacheable (Surrogate-Key tagging,
xkey purges and ESI fragments). Without it that plugin caches no pages and turns every includeEsiBlock() into a
loopback HTTP request, so any site that calls setCacheControl() in its templates needs this component.
What it creates (all in the project namespace):
| Object | Name | Purpose |
|---|---|---|
StatefulSet | varnish | replicaCount pods, varnish:9.1 (bundles the xkey, cookie and std vmods) plus reloader and metrics sidecars |
Service | varnish-service | port 80 → listener http (6081). Point the ingress here |
Service (headless) | varnish-headless | per-pod DNS varnish-0.varnish-headless, … on listener purge (6082). The app sends PURGE here |
ConfigMap | varnish-vcl-default | the reference VCL, only when vcl.existingConfigMap is empty |
PodDisruptionBudget | varnish-pdb | maxUnavailable: 1 |
PodMonitor | varnish | Prometheus scrapes the metrics sidecar (port 9131) of every pod |
Traffic path: Traefik → varnish-service → Varnish → app-service:80. Static assets keep going straight to
app-service through the cache-static IngressRoute of component-traefik-ingress.
Enable it
component-varnish:
enabled: true
component-traefik-ingress:
hosts:
- host: www.example.com
paths:
- path: /
pathType: Prefix
service:
name: varnish-service
port: 80
And in Infisical, for the app:
VARNISH_SERVER=http://varnish-0.varnish-headless:6082,http://varnish-1.varnish-headless:6082
VARNISH_DEFAULT_MAX_AGE=86400
VARNISH_SERVER lists pods by nameThe cacheable plugin fans a purge out to every comma-separated entry. List exactly replicaCount pods
(varnish-0 … varnish-N), without a trailing slash. A pod that is missing from the list keeps serving stale
pages after an editor saves.
Defaults
image: { repository: varnish, tag: "9.1" }
replicaCount: 2
storage: "malloc,512m" # main cache
transientStorage: "malloc,128m" # uncacheable / short-lived objects, capped so passes can't exhaust the pod
extraArgs: ["-p", "thread_pool_min=50", "-p", "thread_pool_max=1000"]
resources: { requests: { cpu: 100m, memory: 768Mi }, limits: { memory: 1Gi } }
ports: { http: 6081, purge: 6082, admin: 6083 }
vcl: { existingConfigMap: "", key: default.vcl }
reloader: { enabled: true, pollSeconds: 5 }
metrics:
enabled: true
exporterImage: { repository: ghcr.io/otto-de/prometheus-varnish-exporter, tag: "1.8.3-varnish-9.0.0" }
port: 9131
interval: 30s
resources: { requests: { cpu: 10m, memory: 32Mi }, limits: { memory: 64Mi } }
pdb: { enabled: true, maxUnavailable: 1 }
Keep resources.limits.memory above storage + transientStorage + ~128Mi.
Purge isolation
PURGE is only honoured on the purge listener. Traefik reaches the http listener, so a PURGE from the
internet gets a 405 whatever its source IP. Inside the namespace the app talks to varnish-headless:6082
directly. The headless Service publishes not-ready pods too, so a purge during a rolling restart still reaches
every replica.
Supported purge requests (what oym/craft-cacheable sends):
| Header | Effect |
|---|---|
xkey-purge: tag1 tag2 | drop every object tagged with one of the keys (Surrogate-Key from Craft) |
X-Regex-Url: <regex> | ban every object whose URL matches |
| none | ban every object whose URL matches the request URL |
Owning the VCL
The chart ships a reference VCL (k8s/helm/charts/components/varnish/files/default.vcl). Most sites need their
own rules (legacy redirects, blocked paths), so the VCL is meant to live as a file in the project's overlays
directory, one per environment:
k8s/app/platform/<slug>/
├── definition/xplatformapp.yaml ← overlaysDir: k8s/app/platform/<slug>/overlays
├── helm/values-*.yaml ← component-varnish.vcl.existingConfigMap: varnish-vcl
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── default.vcl
└── production/
├── kustomization.yaml
└── default.vcl
kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true # fixed name, the reloader sidecar picks up changes
configMapGenerator:
- name: varnish-vcl
files:
- default.vcl
Kustomize generators can only read files inside their own directory, hence one copy per environment. Start from
the reference VCL and put site rules in the marked Site redirects block:
if (req.http.x-path ~ "^/old-page/$") { return (synth(301, "/new-page/")); }
if (req.http.x-path ~ "^/gone/") { return (synth(302, "https://other.example.com/")); }
synth(301|302, "<path or url>") is turned into a redirect by vcl_synth; the query string is dropped, like the
Apache rules this replaces.
How an edit goes live
- Push the changed
default.vcl; ArgoCD syncs the ConfigMap. - Kubelet updates the mounted file inside the pods (up to a minute).
- The
reloadersidecar notices and runsvarnishreload. No pod restart, no cache loss. - A VCL that does not compile is rejected and the previous VCL stays active. Check with:
kubectl -n <slug>-<env> logs varnish-0 -c reloader
kubectl -n <slug>-<env> exec varnish-0 -c varnish -- varnishadm -n /var/lib/varnish vcl.list
Compile-check locally before pushing:
docker run --rm --add-host app-service:127.0.0.1 \
-v "$PWD/default.vcl:/etc/varnish/default.vcl:ro" varnish:9.1 \
varnishd -C -f /etc/varnish/default.vcl > /dev/null
What the reference VCL does
- Health:
/pinganswers 200Pongwithout a backend; it is the startup, readiness and liveness probe. Readiness is deliberately not tied to backend health, so Varnish keeps serving grace while the app is down. - Grace: objects are kept 6h; a healthy backend gets at most 120s of stale, a sick one the full 6h.
- ESI is on for every response;
Surrogate-Keybecomesxkey. - Cookies are stripped except on
/esm,/actions,/logout(control panel and action requests pass through with onlyCraftSessionId);Set-Cookienever enters the cache. - Trailing-slash redirect (
/page→/page/), skipping files, the CP, actions and the api/cache paths. - Tracking parameters (
utm_*,gclid,fbclid, …) are stripped from the cache key. X-Cache: HIT|MISSandX-Cache-Hitsare added, server headers removed.X-Cache-Skip: 1bypasses the cache;Authorization/X-Sentry-Tokenrequests pass.
Monitoring
Every pod runs a metrics sidecar that turns varnishstat counters into Prometheus metrics (varnish_main_*,
varnish_sma_*, varnish_backend_*). A PodMonitor in the project namespace registers it with the cluster
Prometheus, and Grafana has one dashboard for all sites:
OYM / Varnish Cache — filter on
Site (namespace) and Pod.
How to read it:
| Panel | Healthy | Look closer when |
|---|---|---|
| Hit rate | > 80% for a content site | it drops after a deploy (the app stopped sending Cache-Control, or every page now sets a cookie) |
| Pass share | a few percent (control panel, actions) | it climbs: something marks pages uncacheable, see vcl_backend_response |
| Storage used / LRU evictions | below 100%, zero evictions | evictions are sustained: raise storage (and resources.limits.memory) |
| Fetches refused | 0 | the /health probe of app-service marked it sick and Varnish served grace instead of fetching |
| Child restarts | 0 | varnishd panicked; kubectl logs varnish-N -c varnish |
| Expiry and evictions | steady | a step down in Objects in cache without a restart is a purge or ban |
The hit rate is hits ÷ (hits + misses). Passes, pipes and synthetic responses (/ping, redirects, purge replies)
are never cache lookups and are shown separately.
Each pod's hit rate counts separately: with replicaCount: 2 a page is fetched from the app once per pod, so a
cold second pod halves the hit rate until it has seen the same traffic.
varnishstat can only read the shared memory of a varnishd with the same version. The sidecar therefore runs the
chart's own varnish image and executes the exporter binary that an init container copies out of
metrics.exporterImage, so bumping image.tag never breaks the metrics.
Debugging
# Hit / miss per request
kubectl -n <ns> exec varnish-0 -c varnish -- varnishlog -n /var/lib/varnish -g request -q 'ReqMethod eq PURGE'
# Hit ratio
kubectl -n <ns> exec varnish-0 -c varnish -- varnishstat -n /var/lib/varnish -1 -f MAIN.cache_hit -f MAIN.cache_miss
# Backend health
kubectl -n <ns> exec varnish-0 -c varnish -- varnishadm -n /var/lib/varnish backend.list