varnish config

This commit is contained in:
Mateusz Gruszczyński
2025-09-25 09:18:58 +02:00
parent 34d3f3ae0c
commit 113f9e385e

View File

@@ -3,17 +3,22 @@ vcl 4.1;
import vsthrottle; import vsthrottle;
import std; import std;
# ===== Backend =====
backend app { backend app {
.host = "app"; .host = "app";
.port = "${APP_PORT}"; .port = "${APP_PORT}";
} }
/* unikamy duplikatu; dodajemy IPv6 */ # ===== ACL =====
acl purge { "127.0.0.1"; "::1"; } acl purge {
"127.0.0.1";
"::1";
}
# ===== RECV =====
sub vcl_recv { sub vcl_recv {
# RATE LIMIT: 100 żądań / 10s, blokada 60s # RATE LIMIT: 50 żądań / 10s, blokada 60s
if (vsthrottle.is_denied(client.identity, 100, 10s, 60s)) { if (vsthrottle.is_denied(client.identity, 50, 10s, 60s)) {
return (synth(429, "Too Many Requests")); return (synth(429, "Too Many Requests"));
} }
@@ -23,71 +28,163 @@ sub vcl_recv {
return (purge); return (purge);
} }
# omijamy cache dla healthchecków / wewn. nagłówka # omijamy cache dla healthchecków / wewnętrznych nagłówków
if (req.url == "/healthcheck" || req.http.X-Internal-Check) { return (pass); } if (req.url == "/healthcheck" || req.http.X-Internal-Check) { return (pass); }
# Specjalna obsługa WebSocket i socket.io
if (req.http.Upgrade ~ "(?i)websocket" || req.url ~ "^/socket.io/") {
return (pipe);
}
# metody inne niż GET/HEAD bez cache # metody inne niż GET/HEAD bez cache
if (req.method != "GET" && req.method != "HEAD") { return (pass); } if (req.method != "GET" && req.method != "HEAD") { return (pass); }
# statyczne agresywny cache # ---- Normalizacja Accept-Encoding (kolejność preferencji: zstd > br > gzip) ----
if (req.url ~ "^/static/" || req.url ~ "\.(css|js|png|jpg|svg|ico|woff2?)$") { if (req.http.Accept-Encoding) {
if (req.http.Accept-Encoding ~ "zstd") {
set req.http.Accept-Encoding = "zstd";
} else if (req.http.Accept-Encoding ~ "br") {
set req.http.Accept-Encoding = "br";
} else if (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else {
set req.http.Accept-Encoding = "identity";
}
}
# ---- (Opcjonalnie) Normalizacja Accept dla obrazów generowanych wariantowo ----
# Jeśli backend renderuje różne formaty obrazów (webp/jpg) pod tym samym URL (bez rozszerzenia),
# można włączyć „dwustanowy” sygnał do hasha:
#
# if (req.url ~ "\.(png|jpe?g|gif|bmp)$") {
# if (req.http.Accept ~ "image/webp") {
# set req.http.X-Accept-Image = "modern"; # webp
# } else {
# set req.http.X-Accept-Image = "legacy"; # jpg/png
# }
# }
# ---- STATYCZNE agresywny cache + ignorujemy sesję ----
if (req.url ~ "^/static/" || req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") {
unset req.http.Cookie;
unset req.http.Authorization;
return (hash); return (hash);
} }
return (hash); return (hash);
} }
# ===== PIPE (WebSocket passthrough) =====
sub vcl_pipe {
if (req.http.Upgrade) {
set bereq.http.Upgrade = req.http.Upgrade;
set bereq.http.Connection = req.http.Connection;
}
}
# ===== HASH =====
sub vcl_hash {
hash_data(req.url);
if (req.http.host) { hash_data(req.http.host); } else { hash_data(server.ip); }
# Cookie: zostają dla dynamicznych (dla statyków wyczyszczone wcześniej)
if (req.http.Cookie) { hash_data(req.http.Cookie); }
# Accept-Encoding: już znormalizowany do zstd/br/gzip/identity
if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); }
# (Opcjonalnie) sygnał obrazów z negocjacją po Accept
if (req.http.X-Accept-Image) { hash_data(req.http.X-Accept-Image); }
}
# ===== BACKEND_RESPONSE =====
sub vcl_backend_response { sub vcl_backend_response {
# Treści prywatne / zakazane do cache # Zakaz cache respektujemy
if (beresp.http.Cache-Control ~ "(?i)no-store|private") { if (beresp.http.Cache-Control ~ "(?i)no-store|private") {
set beresp.uncacheable = true; set beresp.uncacheable = true;
set beresp.ttl = 0s; set beresp.ttl = 0s;
return (deliver); return (deliver);
} }
# Preferuj s-maxage, potem max-age # ---- STATYCZNE: zdejmij Set-Cookie i Vary: Cookie, zapewnij TTL ----
if (bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") {
unset beresp.http.Set-Cookie;
# Jeśli backend dodał Vary: Cookie, usuńmy ten element (nie wpływa na statyki)
if (beresp.http.Vary) {
set beresp.http.Vary = regsuball(beresp.http.Vary, "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)", "\1");
set beresp.http.Vary = regsuball(beresp.http.Vary, ",[[:space:]]*,", ",");
set beresp.http.Vary = regsub(beresp.http.Vary, "^[[:space:]]*,[[:space:]]*", "");
set beresp.http.Vary = regsub(beresp.http.Vary, "[[:space:]]*,[[:space:]]*$", "");
if (beresp.http.Vary ~ "^[[:space:]]*$") { unset beresp.http.Vary; }
}
# Jeśli brak kontroli czasu życia ustawiamy twarde wartości
if (!(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) {
set beresp.ttl = 24h;
set beresp.http.Cache-Control = "public, max-age=86400, immutable";
}
set beresp.grace = 1h;
set beresp.keep = 24h;
}
# ---- Ogólne TTL z nagłówków ----
if (beresp.http.Cache-Control ~ "(?i)s-maxage=([0-9]+)") { if (beresp.http.Cache-Control ~ "(?i)s-maxage=([0-9]+)") {
set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*s-maxage=([0-9]+).*", "\1") + "s", 0s); set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*s-maxage=([0-9]+).*", "\1") + "s", 0s);
} else if (beresp.http.Cache-Control ~ "(?i)max-age=([0-9]+)") { } else if (beresp.http.Cache-Control ~ "(?i)max-age=([0-9]+)") {
set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*max-age=([0-9]+).*", "\1") + "s", 0s); set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*max-age=([0-9]+).*", "\1") + "s", 0s);
} else if (beresp.http.Expires) { } else if (beresp.http.Expires) {
# fallback na Expires
set beresp.ttl = std.time(beresp.http.Expires, now) - now; set beresp.ttl = std.time(beresp.http.Expires, now) - now;
if (beresp.ttl < 0s) { set beresp.ttl = 0s; } if (beresp.ttl < 0s) { set beresp.ttl = 0s; }
} else { } else {
# ostateczny fallback if (beresp.ttl <= 0s) { set beresp.ttl = 60s; }
set beresp.ttl = 60s;
} }
# Jeśli immutable zwiększ grace/keep # Immutable => dłuższe grace/keep
if (beresp.http.Cache-Control ~ "(?i)immutable") { if (beresp.http.Cache-Control ~ "(?i)immutable") {
set beresp.grace = 1h; set beresp.grace = 1h;
set beresp.keep = 24h; set beresp.keep = 24h;
} }
# statykom daj minimalne TTL, gdy backend NIE ustawił CC # Kompresja po stronie Varnisha wyłącznie dla klientów akceptujących gzip
if ((bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpg|svg|ico|woff2?)$") # i tylko jeśli backend nie dostarczył już Content-Encoding.
&& !(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) { if (!beresp.http.Content-Encoding && bereq.http.Accept-Encoding ~ "gzip") {
set beresp.ttl = 24h; # Kompresujemy tylko „tekstowe” typy
if (beresp.http.Content-Type ~ "(?i)text/|application/(javascript|json|xml|wasm)") {
set beresp.do_gzip = true;
}
}
# Duże odpowiedzi streamujemy
if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) {
set beresp.do_stream = true;
} }
} }
# ===== DELIVER =====
sub vcl_deliver { sub vcl_deliver {
if (obj.hits > 0) { if (obj.uncacheable) {
set resp.http.X-Cache = "PASS";
unset resp.http.Age;
} else if (obj.hits > 0) {
set resp.http.X-Cache = "HIT"; set resp.http.X-Cache = "HIT";
# ukryj niepotrzebny nagłówek z MISS } else {
#} else { set resp.http.X-Cache = "MISS";
# set resp.http.X-Cache = "MISS"; unset resp.http.Age;
} }
# Nagłówki rate limit MUSZĄ używać tej samej czwórki parametrów co is_denied()
#set resp.http.X-RateLimit-Limit = "100";
#set resp.http.X-RateLimit-Window = "10s";
#set resp.http.X-RateLimit-Remaining = vsthrottle.remaining(client.identity, 100, 10s, 60s);
#set resp.http.Retry-After = vsthrottle.blocked(client.identity, 100, 10s, 60s);
unset resp.http.Via; unset resp.http.Via;
unset resp.http.X-Varnish; unset resp.http.X-Varnish;
#unset resp.http.Age;
unset resp.http.Server; unset resp.http.Server;
} }
sub vcl_synth {
set resp.http.X-Cache = "SYNTH";
}
# ===== PURGE HANDLER =====
sub vcl_purge {
return (synth(200, "Purged"));
}