Setting Up Varnish for Magento 2 Caching
Varnish is HTTP accelerator working before web server. For Magento 2 it's standard production stack: without it site with 500+ concurrent visitors saturates CPU on PHP-FPM. Magento 2 has built-in Varnish support via ESI and X-Magento-Tags for cache invalidation.
Stack Architecture
Browser → Varnish :80/:443 → Nginx :8080 → PHP-FPM → MySQL/Redis
↘ ESI requests (cart blocks, auth)
Varnish listens on 80/443, nginx moved to port 8080 receiving only Varnish traffic. For SSL termination, nginx or HAProxy on 443 proxies to Varnish 80.
Varnish Installation
# Ubuntu 22.04
curl -s https://packagecloud.io/install/repositories/varnishcache/varnish74/script.deb.sh | sudo bash
apt install varnish
# Service file
systemctl edit varnish
[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd \
-a :80 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-s malloc,2g \
-p thread_pools=2 \
-p thread_pool_max=1000 \
-p thread_pool_timeout=300
Size -s malloc,2g—for Magento cache 2–4 GB sufficient. Less than 1 GB pointless: Magento pages weigh 80–200 KB and small cache means low hit rate.
VCL Configuration for Magento 2
Magento 2 provides ready VCL via admin: Stores > Configuration > Advanced > System > Full Page Cache > Varnish Configuration > Export VCL. Export and use as base, but standard VCL needs fixes.
Key VCL sections:
vcl 4.1;
import std;
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 600s;
.first_byte_timeout = 600s;
.between_bytes_timeout = 600s;
}
acl purge {
"localhost";
"127.0.0.1";
}
sub vcl_recv {
# Pass real IP
if (req.restarts == 0) {
if (req.http.X-Forwarded-For) {
set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
}
# PURGE requests from Magento
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return (synth(405, "Not allowed"));
}
return (purge);
}
# BAN by X-Magento-Tags (block invalidation)
if (req.method == "BAN") {
if (!client.ip ~ purge) {
return (synth(405, "Not allowed"));
}
if (req.http.X-Magento-Tags-Pattern) {
ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
}
return (synth(200, "Banned"));
}
# Don't cache HTTPS indicator
if (req.http.X-Forwarded-Proto == "https") {
set req.http.Ssl-Offloaded = "1";
}
# Don't cache cart, checkout, account
if (req.url ~ "/(checkout|customer|account|cart|wishlist)") {
return (pass);
}
# Remove cookies on static resources
if (req.url ~ "\.(css|js|png|jpg|jpeg|webp|gif|ico|woff2|svg)(\?.*)?$") {
unset req.http.Cookie;
return (hash);
}
# Normalize Google Analytics parameters
set req.url = regsuball(req.url, "(^|&)(utm_[a-z]+|gclid|gclsrc|fbclid)=[^&]*", "");
set req.url = regsub(req.url, "^(.*)\?&?(.*)?$", "\1?\2");
set req.url = regsub(req.url, "^(.*)\?$", "\1");
return (hash);
}
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
# Different cache for HTTP/HTTPS
if (req.http.Ssl-Offloaded) {
hash_data(req.http.Ssl-Offloaded);
}
return (lookup);
}
sub vcl_backend_response {
# Don't cache 5xx
if (beresp.status >= 500) {
set beresp.uncacheable = true;
set beresp.ttl = 1s;
return (deliver);
}
# TTL by content type
if (beresp.http.content-type ~ "text/html") {
set beresp.ttl = 1d;
set beresp.grace = 1h;
}
if (bereq.url ~ "\.(css|js|woff2)(\?.*)?$") {
set beresp.ttl = 1y;
}
# Remove cookies from cached responses
if (beresp.ttl > 0s) {
unset beresp.http.Set-Cookie;
}
return (deliver);
}
sub vcl_deliver {
# Debug headers (disable on prod)
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
# Remove Magento internal headers
unset resp.http.X-Magento-Tags;
unset resp.http.X-Powered-By;
unset resp.http.Server;
return (deliver);
}
Magento 2 Configuration
Admin > Stores > Configuration > Advanced > System > Full Page Cache:
- Caching Application: Varnish Cache
- TTL for public content: 86400 (24 hours)
Admin > Stores > Configuration > Advanced > System > Full Page Cache > Varnish Configuration:
- Access list:
127.0.0.1 - Backend host:
127.0.0.1 - Backend port:
8080
Enable FPC:
bin/magento cache:enable full_page
bin/magento config:set system/full_page_cache/caching_application 2
# 2 = Varnish, 1 = Magento built-in
ESI — Dynamic Blocks Inside Cached Pages
Magento 2 uses ESI (Edge Side Includes) for personalized blocks: cart, username, wishlist. ESI already enabled in VCL via beresp.do_esi = true, which Magento sets in X-Esi: 1 header.
Check ESI works:
curl -I https://myshop.ru/ | grep X-Cache
# Should return: X-Cache: HIT
curl -I https://myshop.ru/checkout/cart/ | grep X-Cache
# Should return: X-Cache: MISS (cart not cached)
Cache Invalidation
Magento auto-invalidates cache on product/category changes via BAN by tags:
# Manual invalidate specific page
curl -X PURGE http://127.0.0.1:80/catalog/product/view/id/42
# Invalidate all cache
varnishadm "ban req.url ~ /"
# Cache statistics
varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss
Target hit rate for Magento—85–95%. Below 70% indicates problem: either too many uncacheable requests or VCL handles cookies incorrectly.
Monitoring
# Real-time requests and status
varnishlog -q 'RespHeader:X-Cache' -i RespHeader,ReqURL,RespStatus
# Top uncacheable URLs
varnishlog -q 'RespHeader:X-Cache eq MISS' -i ReqURL | \
awk '{print $2}' | sort | uniq -c | sort -rn | head -20
Nginx as SSL Terminator
server {
listen 443 ssl http2;
server_name myshop.ru;
ssl_certificate /etc/letsencrypt/live/myshop.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myshop.ru/privkey.pem;
location / {
proxy_pass http://127.0.0.1:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Ssl-Offloaded "1";
}
}
Timeline
Varnish installation, VCL config for Magento 2, nginx move to 8080, SSL termination setup: 1–2 days. Hit rate testing, invalidation, ESI blocks: 1 day. Load testing and parameter tuning (malloc, thread_pools): 0.5–1 day.







