From e5929ce98fb4d3f3a77337f507c01281a2dfe6d3 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Fri, 12 Apr 2024 16:57:06 +0200 Subject: [PATCH] initial commit --- .github/workflows/docker.yml | 28 +++ Dockerfile | 18 ++ README.md | 22 ++ rootfs/etc/varnish/default.vcl | 209 ++++++++++++++++++ .../usr/local/bin/docker-varnish-entrypoint | 26 +++ 5 files changed, 303 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 rootfs/etc/varnish/default.vcl create mode 100755 rootfs/usr/local/bin/docker-varnish-entrypoint diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..56f7f04 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,28 @@ +name: Build Docker + +on: + push: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login into Github Docker Registery + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build Docker Images + uses: docker/build-push-action@v4 + with: + tags: ghcr.io/shopwarelabs/varnish + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + push: true + provenance: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ed2a597 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM varnish:fresh-alpine + +# install build dependencies +USER root +RUN set -e; \ + apk upgrade --no-cache; \ + apk add --no-cache $VMOD_DEPS; \ + \ +# install one, possibly multiple vmods + install-vmod https://github.com/varnish/varnish-modules/releases/download/0.24.0/varnish-modules-0.24.0.tar.gz; \ + \ +# clean up + apk del --no-network $VMOD_DEPS +USER varnish +ENV SHOPWARE_BACKEND_HOST=localhost \ + SHOPWARE_BACKEND_PORT=8000 + +COPY --chown=1000 rootfs / diff --git a/README.md b/README.md new file mode 100644 index 0000000..05359fc --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Shopware Varnish Docker image + +The image bases on the official [Varnish image](https://hub.docker.com/_/varnish) and contains the Shopware default VCL. + +## Additional environment variables + +- `SHOPWARE_BACKEND_HOST` - The host of the Shopware backend. Default: `localhost` +- `SHOPWARE_BACKEND_PORT` - The port of the Shopware backend. Default: `8000` +- `SHOPWARE_SOFT_PURGE` - If set to `1`, the soft purge feature is enabled. Default: `0` + +## Example usage + +```bash +docker run \ + --rm \ + -it \ + # host ip where Shopware is + -e SHOPWARE_BACKEND_HOST=host.docker.internal \ + -p 8080:80 \ + --name=varnish \ + varnish +``` diff --git a/rootfs/etc/varnish/default.vcl b/rootfs/etc/varnish/default.vcl new file mode 100644 index 0000000..92e2a4f --- /dev/null +++ b/rootfs/etc/varnish/default.vcl @@ -0,0 +1,209 @@ +vcl 4.1; + +import std; +import xkey; + +# Specify your app nodes here. Use round-robin balancing to add more than one. +backend default { + .host = "__SHOPWARE_BACKEND_HOST__"; + .port = "__SHOPWARE_BACKEND_PORT__"; +} + +# ACL for purgers IP. (This needs to contain app server ips) +acl purgers { + "127.0.0.1"; + "localhost"; + "::1"; +} + +sub vcl_recv { + # Mitigate httpoxy application vulnerability, see: https://httpoxy.org/ + unset req.http.Proxy; + + # Ignore query strings that are only necessary for the js on the client. Customize as needed. + if (req.url ~ "(\?|&)(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=") { + # see rfc3986#section-2.3 "Unreserved Characters" for regex + set req.url = regsuball(req.url, "(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=[A-Za-z0-9\-\_\.\~]+&?", ""); + } + set req.url = regsub(req.url, "(\?|\?&|&)$", ""); + + # Normalize query arguments + set req.url = std.querysort(req.url); + + # Set a header announcing Surrogate Capability to the origin + set req.http.Surrogate-Capability = "shopware=ESI/1.0"; + + # Make sure that the client ip is forward to the client. + 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; + } + + # Handle PURGE + if (req.method == "PURGE") { + if (client.ip !~ purgers) { + return (synth(403, "Forbidden")); + } + if (req.http.xkey) { + set req.http.n-gone = xkey.purge(req.http.xkey); + + return (synth(200, "Invalidated "+req.http.n-gone+" objects")); + } else { + return (purge); + } + } + + if (req.method == "BAN") { + if (!client.ip ~ purgers) { + return (synth(405, "Method not allowed")); + } + + ban("req.url ~ "+req.url); + return (synth(200, "BAN URLs containing (" + req.url + ") done.")); + } + + # Normalize Accept-Encoding header + # straight from the manual: https://www.varnish-cache.org/docs/3.0/tutorial/vary.html + if (req.http.Accept-Encoding) { + if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") { + # No point in compressing these + unset req.http.Accept-Encoding; + } elsif (req.http.Accept-Encoding ~ "gzip") { + set req.http.Accept-Encoding = "gzip"; + } elsif (req.http.Accept-Encoding ~ "deflate") { + set req.http.Accept-Encoding = "deflate"; + } else { + # unknown algorithm + unset req.http.Accept-Encoding; + } + } + + if (req.method != "GET" && + req.method != "HEAD" && + req.method != "PUT" && + req.method != "POST" && + req.method != "TRACE" && + req.method != "OPTIONS" && + req.method != "PATCH" && + req.method != "DELETE") { + /* Non-RFC2616 or CONNECT which is weird. */ + return (pipe); + } + + # We only deal with GET and HEAD by default + if (req.method != "GET" && req.method != "HEAD") { + return (pass); + } + + # Don't cache Authenticate & Authorization + if (req.http.Authenticate || req.http.Authorization) { + return (pass); + } + + # Always pass these paths directly to php without caching + # Note: virtual URLs might bypass this rule (e.g. /en/checkout) + if (req.url ~ "^/(checkout|account|admin|api)(/.*)?$") { + return (pass); + } + + return (hash); +} + +sub vcl_hash { + # Consider Shopware HTTP cache cookies + if (req.http.cookie ~ "sw-cache-hash=") { + hash_data("+context=" + regsub(req.http.cookie, "^.*?sw-cache-hash=([^;]*);*.*$", "\1")); + } elseif (req.http.cookie ~ "sw-currency=") { + hash_data("+currency=" + regsub(req.http.cookie, "^.*?sw-currency=([^;]*);*.*$", "\1")); + } +} + +sub vcl_hit { + # Consider client states for response headers + if (req.http.cookie ~ "sw-states=") { + set req.http.states = regsub(req.http.cookie, "^.*?sw-states=([^;]*);*.*$", "\1"); + + if (req.http.states ~ "logged-in" && obj.http.sw-invalidation-states ~ "logged-in" ) { + return (pass); + } + + if (req.http.states ~ "cart-filled" && obj.http.sw-invalidation-states ~ "cart-filled" ) { + return (pass); + } + } +} + +sub vcl_backend_response { + # Fix Vary Header in some cases + # https://www.varnish-cache.org/trac/wiki/VCLExampleFixupVary + if (beresp.http.Vary ~ "User-Agent") { + set beresp.http.Vary = regsub(beresp.http.Vary, ",? *User-Agent *", ""); + set beresp.http.Vary = regsub(beresp.http.Vary, "^, *", ""); + if (beresp.http.Vary == "") { + unset beresp.http.Vary; + } + } + + if (beresp.http.Surrogate-Control ~ "ESI/1.0") { + unset beresp.http.Surrogate-Control; + set beresp.do_esi = true; + return (deliver); + } + + # Respect the Cache-Control=private header from the backend + if ( + beresp.http.Pragma ~ "no-cache" || + beresp.http.Cache-Control ~ "no-cache" || + beresp.http.Cache-Control ~ "private" + ) { + set beresp.ttl = 0s; + set beresp.http.X-Cacheable = "NO:Cache-Control=private"; + set beresp.uncacheable = true; + return (deliver); + } + + # strip the cookie before the image is inserted into cache. + if (bereq.url ~ "\.(png|gif|jpg|swf|css|js|webp)$") { + unset beresp.http.set-cookie; + } + + # Allow items to be stale if needed. + set beresp.grace = 6h; + + # Save the bereq.url so bans work efficiently + set beresp.http.x-url = bereq.url; + set beresp.http.X-Cacheable = "YES"; + + # Remove the exact PHP Version from the response for more security + unset beresp.http.x-powered-by; + + return (deliver); +} + +sub vcl_deliver { + ## we don't want the client to cache + set resp.http.Cache-Control = "max-age=0, private"; + + # remove link header, if session is already started to save client resources + if (req.http.cookie ~ "session-") { + unset resp.http.Link; + } + + # Set a cache header to allow us to inspect the response headers during testing + if (obj.hits > 0) { + unset resp.http.set-cookie; + set resp.http.X-Cache = "HIT"; + } else { + set resp.http.X-Cache = "MISS"; + } + + # Remove the exact PHP Version from the response for more security (e.g. 404 pages) + unset resp.http.x-powered-by; + + # invalidation headers are only for internal use + unset resp.http.sw-invalidation-states; + unset resp.http.xkey; + + set resp.http.X-Cache-Hits = obj.hits; +} diff --git a/rootfs/usr/local/bin/docker-varnish-entrypoint b/rootfs/usr/local/bin/docker-varnish-entrypoint new file mode 100755 index 0000000..88f6ae2 --- /dev/null +++ b/rootfs/usr/local/bin/docker-varnish-entrypoint @@ -0,0 +1,26 @@ +#!/usr/bin/env sh + +set -eo pipefail + +sed -i "s|__SHOPWARE_BACKEND_HOST__|${SHOPWARE_BACKEND_HOST}|g" /etc/varnish/default.vcl +sed -i "s|__SHOPWARE_BACKEND_PORT__|${SHOPWARE_BACKEND_PORT}|g" /etc/varnish/default.vcl + +if [[ "${SHOPWARE_SOFT_PURGE}" ]]; then + sed -i "s|xkey.purge|xkey.softpurge|g" /etc/varnish/default.vcl +fi + +# this will check if the first argument is a flag +# but only works if all arguments require a hyphenated flag +# -v; -SL; -f arg; etc will work, but not arg1 arg2 +if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then + set -- varnishd \ + -F \ + -f /etc/varnish/default.vcl \ + -a http=:${VARNISH_HTTP_PORT:-80},HTTP \ + -a proxy=:${VARNISH_PROXY_PORT:-8443},PROXY \ + -p feature=+http2 \ + -s malloc,$VARNISH_SIZE \ + "$@" +fi + +exec "$@"