From e8957e0672f06c4b20231dc4c53f14246610d689 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 5 Feb 2026 01:54:17 +0100 Subject: [PATCH] feat: Add Varnish caching service and configuration, adjusting Traefik routing and implementing a rate limit middleware. --- .env.example | 5 +++ docker-compose.yml | 20 +++++++++- varnish/default.vcl | 92 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 varnish/default.vcl diff --git a/.env.example b/.env.example index f09355da..f2fa2702 100644 --- a/.env.example +++ b/.env.example @@ -57,6 +57,11 @@ IMAGE_TAG=latest TRAEFIK_HOST=klz-cables.com ENV_FILE=.env +# ──────────────────────────────────────────────────────────────────────────── +# Varnish Configuration +# ──────────────────────────────────────────────────────────────────────────── +VARNISH_CACHE_SIZE=256M + # ============================================================================ # IMPORTANT NOTES # ============================================================================ diff --git a/docker-compose.yml b/docker-compose.yml index 63024108..91525654 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,20 @@ services: - infra env_file: - ${ENV_FILE:-.env} + labels: + - "traefik.enable=false" + + varnish: + image: varnish:7 + restart: always + networks: + - infra + volumes: + - ./varnish/default.vcl:/etc/varnish/default.vcl:ro + tmpfs: + - /var/lib/varnish:exec + environment: + VARNISH_SIZE: ${VARNISH_CACHE_SIZE:-256M} labels: - "traefik.enable=true" # HTTP ⇒ HTTPS redirect @@ -18,13 +32,13 @@ services: - "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls.certresolver=le" - "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls=true" - "traefik.http.routers.${PROJECT_NAME:-klz-cables}.service=${PROJECT_NAME:-klz-cables}" - - "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.port=3000" + - "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.port=80" - "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.scheme=http" # Forwarded Headers - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Proto=https" - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on" # Middlewares - - "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-compress}" + - "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-compress}" # Gatekeeper Router (to show the login page) - "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=Host(${TRAEFIK_HOST}) && PathPrefix(`/gatekeeper`)" @@ -34,6 +48,8 @@ services: - "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.service=${PROJECT_NAME:-klz-cables}-gatekeeper" # Middleware Definitions + - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-ratelimit.ratelimit.average=100" + - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-ratelimit.ratelimit.burst=50" - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.address=http://${PROJECT_NAME}-gatekeeper:3000/verify" - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.trustForwardHeader=true" - "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authResponseHeaders=X-Auth-User" diff --git a/varnish/default.vcl b/varnish/default.vcl new file mode 100644 index 00000000..846b58e3 --- /dev/null +++ b/varnish/default.vcl @@ -0,0 +1,92 @@ +vcl 4.1; + +import std; + +backend default { + .host = "app"; + .port = "3000"; + .first_byte_timeout = 60s; +} + +acl purge { + "localhost"; + "127.0.0.1"; + "infra"; # Allow purge from within the infra network +} + +sub vcl_recv { + # Only allow PURGE from the ACL + if (req.method == "PURGE") { + if (!client.ip ~ purge) { + return (synth(405, "Not allowed.")); + } + return (purge); + } + + # Only cache GET and HEAD requests + if (req.method != "GET" && req.method != "HEAD") { + return (pass); + } + + # Bypass cache for Gatekeeper and Directus (already handled by Traefik, but safe to be explicit) + if (req.url ~ "^/gatekeeper" || req.url ~ "^/directus" || req.url ~ "^/admin") { + return (pass); + } + + # Bypass cache for Next.js preview mode / health checks + if (req.url ~ "^/api/preview" || req.url ~ "^/health") { + return (pass); + } + + # Remove all cookies for static files to improve cache hits + if (req.url ~ "\.(png|gif|jpg|jpeg|svg|ico|webp|js|css|woff|woff2|otf|ttf)$") { + unset req.http.Cookie; + } + + # Normalize Cookies: Remove tracking cookies that don't affect page content + # This keeps cookies like NEXT_LOCALE or AUTH cookies if needed, but strips others + if (req.http.Cookie) { + # Strip Google Analytics cookies + set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(__utm.|_ga.|_gid.|_gat)(=[^;]*)?", ""); + # Strip empty cookies + set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", ""); + if (req.http.Cookie ~ "^\s*$") { + unset req.http.Cookie; + } + } + + return (hash); +} + +sub vcl_backend_response { + # Cache static assets for a long time + if (bereq.url ~ "\.(png|gif|jpg|jpeg|svg|ico|webp|js|css|woff|woff2|otf|ttf)$") { + set beresp.ttl = 1w; + } + + # Respect Cache-Control from Next.js + # If the response should not be cached, Next.js will usually send Cache-Control: no-cache, no-store, etc. + if (beresp.http.Cache-Control ~ "private" || + beresp.http.Cache-Control ~ "no-cache" || + beresp.http.Cache-Control ~ "no-store") { + set beresp.uncacheable = true; + return (deliver); + } + + # Set a default TTL if none is provided by the backend + if (beresp.ttl <= 0s) { + set beresp.ttl = 120s; + } + + return (deliver); +} + +sub vcl_deliver { + # Add a debug header to show if it was a hit or miss + 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"; + } +}