From 7d1f536af5ccc431ec2dac624f347b05ac32fb6d Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Thu, 21 Aug 2025 23:41:26 +0200 Subject: [PATCH] proxy through varnish sets up varnish cache server with: - custom cache per content type - gzip - configure app & hass backends - varnish debug headers & X-Cache verbose cache header also updates docker-compose, varnish/Dockerfile & drone. --- .drone.yml | 41 +++++++- docker-compose.yml | 21 ++++ src/lib/components/LiveImage.svelte | 3 +- varnish/Dockerfile | 2 + varnish/default.vcl | 146 ++++++++++++++++++++++++++++ varnish/includes/x-cache-header.vcl | 43 ++++++++ varnish/vcl_deliver.vcl | 40 ++++++++ vite.config.ts | 7 +- 8 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 docker-compose.yml create mode 100644 varnish/default.vcl create mode 100644 varnish/includes/x-cache-header.vcl create mode 100644 varnish/vcl_deliver.vcl diff --git a/.drone.yml b/.drone.yml index a3468fc..6ffbdb1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -48,6 +48,44 @@ steps: - latest - ${DRONE_COMMIT_SHA} +trigger: + event: + include: + - push + exclude: + - pull_request + branch: + - main + - update + +depends_on: + - Build + +--- +kind: pipeline +type: docker +name: Publish + +platform: + os: linux + arch: amd64 +kind: pipeline +type: docker +name: config-check + +steps: + - name: check-config + image: alpine/git + commands: + - git fetch --no-tags --depth=2 + - | + if git diff --quiet HEAD^ HEAD -- varnish/default.vcl; then + echo "No changes in varnish config file, skipping..." + exit 78 # exit code 78 = skip in Drone + else + echo "Changes detected in varnish config" + fi + - name: Publish varnish to ghcr image: plugins/docker settings: @@ -72,7 +110,6 @@ trigger: branch: - main - update - depends_on: - Build @@ -146,6 +183,6 @@ volumes: temp: {} --- kind: signature -hmac: b3cc991813f340024e65c68d5509cb23025796914a2e2ac72f71657a347e0708 +hmac: 01caa41521eac62356f6fc941cdd489dae8e2c4249bdb4e4dc1a32e101c639b7 ... diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ce83ef9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + app: + build: . + container_name: infra-map + ports: + - '3000:3000' # svelte-kit preview HTTP + varnish: + build: varnish + container_name: varnish-cache + ports: + - '6081:6081' # Varnish HTTP + environment: + - VARNISH_LISTEN_PORT=6081 + command: > + varnishd + -F + -f /etc/varnish/default.vcl + -s malloc,256m + -a :6081 diff --git a/src/lib/components/LiveImage.svelte b/src/lib/components/LiveImage.svelte index 48d1404..760e731 100644 --- a/src/lib/components/LiveImage.svelte +++ b/src/lib/components/LiveImage.svelte @@ -33,7 +33,8 @@ function refetchImage() { let url; try { - url = new URL(`${IMAGE_PROXY_URL}/image/${imageUrl}`); + const { protocol, host } = window.location; + url = new URL(`${protocol}//${host}/image-proxy/${imageUrl}`); } catch { console.log('url not valid, returning'); return; diff --git a/varnish/Dockerfile b/varnish/Dockerfile index 0752b03..af867b3 100644 --- a/varnish/Dockerfile +++ b/varnish/Dockerfile @@ -21,6 +21,8 @@ RUN git clone https://github.com/varnish/libvmod-digest.git /opt/libvmod-digest ./configure VARNISHSRC=/usr/include/varnish && \ make && make install +COPY . /etc/varnish/ + EXPOSE 6081 CMD ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-a", ":6081", "-s", "malloc,512m"] diff --git a/varnish/default.vcl b/varnish/default.vcl new file mode 100644 index 0000000..d69ff98 --- /dev/null +++ b/varnish/default.vcl @@ -0,0 +1,146 @@ +vcl 4.0; + +import std; +import digest; + +# include "handlers/ttl-override-handler.vcl"; +include "includes/x-cache-header.vcl"; + +include "vcl_deliver.vcl"; + +# Define backend pointing to Home Assistant IP +backend hass_backend { + .host = "10.0.0.82"; + .port = "8123"; +} + +backend app_frontend { + .host = "host.docker.internal"; + .port = "5173"; +} + +sub vcl_recv { + set req.backend_hint = app_frontend; + + # Handle CORS preflight + if (req.method == "OPTIONS") { + return (synth(204, "Preflight")); + } + + # Rewrite image URL + if (req.url ~ "^/image-proxy/?") { + # Extract everything after /image-proxy/ and store it + set req.http.X-Image-URL = regsub(req.url, "^/image-proxy/(.*)", "\1"); + + # Rewrite req.url to match backend expectations + set req.url = regsub(req.http.X-Image-URL, "^http://[^/]+", ""); + + set req.backend_hint = hass_backend; + } + + # Enable debug headers through query param + if (req.url ~ "(?i)debug=(true|yes|1)") { + set req.http.X-debug = true; + } + + # Remove cookies so content is cacheable + unset req.http.Cookie; +} + +sub vcl_synth { + if (resp.status == 204) { + set resp.http.Access-Control-Allow-Origin = "*"; + set resp.http.Access-Control-Allow-Methods = "GET, OPTIONS"; + set resp.http.Access-Control-Allow-Headers = "Content-Type, X-Cache-ID"; + set resp.http.Content-Length = "0"; + return (deliver); + } + + if (resp.status == 304) { + set resp.http.ETag = req.http.If-None-Match; + set resp.http.Content-Length = "0"; + return (deliver); + } +} + +sub vcl_backend_response { + ################### + # cache rules # + ################### + # HTML pages → short cache or no cache + if (bereq.url ~ "\.html$") { + set beresp.ttl = 30s; # Cache briefly + set beresp.uncacheable = true; # Or disable cache entirely + } + + # JavaScript & CSS → long cache + if (bereq.url ~ "\.(js|css)$") { + set beresp.ttl = 1d; + } + + # Images under /image/ → long cache + if (bereq.url ~ "^/images/.*\.(svg|png|jpe?g)$") { + set beresp.ttl = 1y; + } + + # Favicons → long cache + if (bereq.url ~ "^/favicons/") { + set beresp.ttl = 1y; + } + + # Fallback: ensure some cache + if (beresp.ttl <= 0s) { + set beresp.ttl = 22s; + } + + set beresp.http.X-TTL = beresp.ttl; + + #################### + # camera proxy # + #################### + if (bereq.url ~ "^/api/camera_proxy/") { + set beresp.ttl = 0.3s; + set beresp.grace = 60s; + set beresp.keep = 60s; + + # Ensure ETag is passed to client + if (beresp.http.ETag) { + set beresp.http.X-Cache-ETag = beresp.http.ETag; + } else { + # Optional: generate one if not provided + # set beresp.http.ETag = digest.hash_md5(beresp.body); + set beresp.http.ETag = beresp.http.Content-Length; + set beresp.http.X-Cache-ETag = beresp.http.ETag; + } + } + + ######################## + # gzip compression # + ######################## + if (beresp.http.Content-Type ~ "^(text/|application/json|application/javascript|application/x-javascript|application/xml|application/xhtml+xml|image/svg\+xml)") { + set beresp.do_gzip = true; + } + + return (deliver); +} + +sub vcl_hit { + if (obj.ttl < 0s && std.healthy(req.backend_hint)) { + return (deliver); + } +} + +sub vcl_deliver { + unset resp.http.X-Image-URL; + set resp.http.Access-Control-Allow-Origin = "*"; + + # Handle conditional request with ETag + if ( + req.http.If-None-Match && + req.http.If-None-Match == resp.http.ETag + ) { + return (synth(304)); + } + + return (deliver); +} diff --git a/varnish/includes/x-cache-header.vcl b/varnish/includes/x-cache-header.vcl new file mode 100644 index 0000000..7f6a2a1 --- /dev/null +++ b/varnish/includes/x-cache-header.vcl @@ -0,0 +1,43 @@ +sub vcl_recv { + unset req.http.X-Cache; +} + +sub vcl_hit { + set req.http.X-Cache = "HIT"; +} + +sub vcl_miss { + set req.http.X-Cache = "MISS"; +} + +sub vcl_pass { + set req.http.X-Cache = "PASS"; +} + +sub vcl_pipe { + set req.http.X-Cache = "PIPE uncacheable"; +} + +sub vcl_synth { + set resp.http.X-Cache = "SYNTH"; +} + +sub vcl_deliver { + if (obj.uncacheable) { + set req.http.X-Cache = req.http.X-Cache + " uncacheable" ; + } else { + set req.http.X-Cache = req.http.X-Cache + " cached" + " (real age: " + resp.http.Age + ", hits: " + obj.hits + ", ttl: " + resp.http.x-ttl + ")"; + } + + # if we are gracing, make sure the browser doesn't cache things, and set our maxage to 1 + # also log grace delivery + if (req.http.graceineffect) { + set resp.http.Cache-Control = regsub(resp.http.Cache-Control, "max-age=[0-9]*", "max-age=1"); + set resp.http.Cache-Control = regsub(resp.http.Cache-Control, "channel-maxage=[0-9]*", "channel-maxage=1"); + set req.http.X-Cache = req.http.X-Cache + " [grace: " + req.http.graceineffect + " " + req.http.grace + ", remaining: " + req.http.graceduration + "]"; + } + + # uncomment the following line to show the information in the response + set resp.http.X-Cache = req.http.X-Cache; +} + diff --git a/varnish/vcl_deliver.vcl b/varnish/vcl_deliver.vcl new file mode 100644 index 0000000..78d8545 --- /dev/null +++ b/varnish/vcl_deliver.vcl @@ -0,0 +1,40 @@ +sub vcl_deliver { + # Happens when we have all the pieces we need, and are about to send the + # response to the client. + + if (resp.status == 503) { + set resp.http.failing-backend = "true"; + } + + # Give some debug + if (req.http.X-debug && req.esi_level == 0) { + set resp.http.X-Backend = req.backend_hint; + set resp.http.X-Backend-Url = req.url; + } else { + # not debug, strip some headers + unset resp.http.X-Cache; + unset resp.http.X-Backend; + unset resp.http.x-upstream; + unset resp.http.x-request-uri; + unset resp.http.Via; + unset resp.http.xkey; + unset resp.http.x-goog-hash; + unset resp.http.x-goog-generation; + unset resp.http.X-GUploader-UploadID; + unset resp.http.x-goog-storage-class; + unset resp.http.x-goog-metageneration; + unset resp.http.x-goog-stored-content-length; + unset resp.http.x-goog-stored-content-encoding; + unset resp.http.x-goog-meta-goog-reserved-file-mtime; + unset resp.http.Server; + unset resp.http.X-Apache-Host; + unset resp.http.X-Varnish-Backend; + unset resp.http.X-Varnish-Host; + unset resp.http.X-Nginx-Host; + unset resp.http.X-Upstream-Age; + unset resp.http.X-Retries; + unset resp.http.X-TTL; + unset resp.http.X-Varnish; + } +} + diff --git a/vite.config.ts b/vite.config.ts index 84286ca..a1c1774 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,11 +4,6 @@ import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { - proxy: { - '/proxmox': { - target: 'https://apollo.schleppe:8006/api2/json', - secure: false - } - } + allowedHosts: ['host.docker.internal'] } });