From 272009c1a59126620d01ac5185f4c5d44ff80074 Mon Sep 17 00:00:00 2001 From: patzick <13100280+patzick@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:14:29 +0200 Subject: [PATCH] feat: docker deployment with fastly setup --- templates/vue-starter-template/.dockerignore | 12 +++++ templates/vue-starter-template/.env.template | 14 +++-- templates/vue-starter-template/Dockerfile | 41 ++++++++++++++ templates/vue-starter-template/README.md | 54 ++++++++++++++++++- templates/vue-starter-template/nuxt.config.ts | 20 ++++++- 5 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 templates/vue-starter-template/.dockerignore create mode 100644 templates/vue-starter-template/Dockerfile diff --git a/templates/vue-starter-template/.dockerignore b/templates/vue-starter-template/.dockerignore new file mode 100644 index 000000000..2d47504b7 --- /dev/null +++ b/templates/vue-starter-template/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.nuxt +.output +.turbo +.vercel +.env +.env.* +!.env.template +dist +coverage +*.log +.DS_Store diff --git a/templates/vue-starter-template/.env.template b/templates/vue-starter-template/.env.template index 3741b4269..19a7c86eb 100644 --- a/templates/vue-starter-template/.env.template +++ b/templates/vue-starter-template/.env.template @@ -4,23 +4,27 @@ # Your Shopware 6 Store API endpoint # https://frontends.shopware.com/installation.html -NUXT_PUBLIC_SHOPWARE_ENDPOINT="https://demo-frontends.shopware.store/store-api/" +NUXT_PUBLIC_SHOPWARE_ENDPOINT=https://demo-frontends.shopware.store/store-api/ # Sales Channel access token (Settings > Sales Channel > API access in Shopware admin) # https://frontends.shopware.com/installation.html -NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN="SWSCNWDGMUWZM0TLVUU0YKLQVW" +NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN=SWSCNWDGMUWZM0TLVUU0YKLQVW # Storefront domain registered in your Sales Channel (Settings > Sales Channel > Domains). # Required for customer registration to work in local development, where localhost # doesn't match any configured domain. In production, window.location.origin is used. # Only set this for local development/customer registration issues. Leave empty in production. -NUXT_PUBLIC_SHOPWARE_DEV_STOREFRONT_URL="" +NUXT_PUBLIC_SHOPWARE_DEV_STOREFRONT_URL= + +# Optional private SSR endpoint. Set this only if server-side requests should +# use a different internal URL than the public browser endpoint. +NUXT_SHOPWARE_ENDPOINT= # Type generation (development only) — used by `pnpm generate-types` (@shopware/api-gen) # https://frontends.shopware.com/packages/api-client.html # Base URL of your Shopware instance (without /store-api/ suffix) -OPENAPI_JSON_URL="https://demo-frontends.shopware.store" +OPENAPI_JSON_URL=https://demo-frontends.shopware.store # Sales Channel access key for fetching the OpenAPI schema -OPENAPI_ACCESS_KEY="" +OPENAPI_ACCESS_KEY= diff --git a/templates/vue-starter-template/Dockerfile b/templates/vue-starter-template/Dockerfile new file mode 100644 index 000000000..ed6154d0d --- /dev/null +++ b/templates/vue-starter-template/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM node:24-alpine AS builder + +RUN corepack enable && corepack prepare pnpm@10.23.0 --activate + +WORKDIR /app + +# Copy package files first for better layer caching +COPY package.json pnpm-lock.yaml* ./ + +RUN pnpm install + +# Copy source files +COPY . . + +RUN pnpm build + +# Production stage +FROM node:24-alpine AS runner + +WORKDIR /app + +# Create tmp directory for runtime writes (mount as tmpfs when running read-only) +RUN mkdir -p /app/tmp && chown node:node /app/tmp + +# Copy built output from builder (standalone, no deps needed) +COPY --from=builder --chown=node:node /app/.output ./.output + +# Use non-root user for security +USER node + +# Point Node.js temp operations to /app/tmp +ENV TMPDIR=/app/tmp + +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV NODE_ENV=production + +EXPOSE 3000 + +CMD ["node", ".output/server/index.mjs"] diff --git a/templates/vue-starter-template/README.md b/templates/vue-starter-template/README.md index 48472dc61..d8edd7337 100644 --- a/templates/vue-starter-template/README.md +++ b/templates/vue-starter-template/README.md @@ -76,6 +76,43 @@ bun run preview Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. +### Docker + +Build and run the production image from this directory: + +```bash +docker build -t shopware-vue-starter . +docker run --rm -p 3000:3000 \ + -e NUXT_PUBLIC_SHOPWARE_ENDPOINT="https://your-shop.example/store-api/" \ + -e NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN="your-sales-channel-token" \ + -e NUXT_PUBLIC_SHOPWARE_DEV_STOREFRONT_URL="" \ + shopware-vue-starter +``` + +You can also pass environment variables from a local `.env` file: + +```bash +docker run --rm -p 3000:3000 --env-file .env shopware-vue-starter +``` + +The Docker image reads the same Nuxt public runtime variables as local development: + +```bash +NUXT_PUBLIC_SHOPWARE_ENDPOINT=https://your-shop.example/store-api/ +NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN=your-sales-channel-token +NUXT_PUBLIC_SHOPWARE_DEV_STOREFRONT_URL= +NUXT_SHOPWARE_ENDPOINT= +``` + +`NUXT_PUBLIC_SHOPWARE_DEV_STOREFRONT_URL` is mainly useful for local development when the Shopware sales channel domain does not match `localhost`. In production, leave it empty unless you specifically need to override it. + +`NUXT_SHOPWARE_ENDPOINT` is optional. Use it only when server-side requests from the container should target a different internal URL than the browser-facing `NUXT_PUBLIC_SHOPWARE_ENDPOINT`. + +The template includes a `.dockerignore` file so local `node_modules`, `.nuxt`, `.output`, and `.env` files are not copied into the image. This is important: + +- your local `.env` must not be baked into the image during `docker build` +- the Shopware instance must be selected at container runtime via `docker run --env-file .env` or `-e ...` + ## Styling and Shopping Experiences integration This tempalte uses [UnoCSS](https://unocss.dev/) for styling, which is a utility-first CSS framework. It is configured to use the [Tailwind CSS](https://tailwindcss.com/) classes. @@ -95,4 +132,19 @@ The extended template: **Use the base template** (this one) when you want to start from scratch with minimal setup and maximum flexibility. -**Use the extended template** when you want to see a complete implementation or need a head start with pre-built components and styling. \ No newline at end of file +**Use the extended template** when you want to see a complete implementation or need a head start with pre-built components and styling. + +## Fastly and ISR + +This template uses Nuxt route rules with ISR for public storefront pages and adds `Surrogate-Control` headers for CDN caching: + +- public storefront routes: `Surrogate-Control: max-age=3600, stale-while-revalidate=86400` +- checkout, account, and wishlist routes: `Surrogate-Control: no-store` + +For a Fastly-backed Node deployment, the usual setup is: + +- cache `GET` and `HEAD` HTML responses when `Surrogate-Control` or cacheable `Cache-Control` is present +- bypass caching for `/checkout`, `/account`, and `/wishlist` +- avoid caching responses that set cookies or depend on user/session-specific SSR + +Nuxt/Nitro provides the cache headers. Fastly still needs to be configured to respect them for HTML responses. diff --git a/templates/vue-starter-template/nuxt.config.ts b/templates/vue-starter-template/nuxt.config.ts index e9db36023..6985a5be2 100644 --- a/templates/vue-starter-template/nuxt.config.ts +++ b/templates/vue-starter-template/nuxt.config.ts @@ -77,8 +77,11 @@ export default defineNuxtConfig({ }, routeRules: { "/**": { - // 60-minute ISR — increase for mostly-static storefronts, decrease for frequently updated content - isr: 60 * 60, + // 24-hour ISR — reduce for frequently updated catalogs or CMS-heavy storefronts + isr: 60 * 60 * 24, + headers: { + "Surrogate-Control": "max-age=86400, stale-while-revalidate=86400", + }, }, "/**/*.svg": { headers: { @@ -89,19 +92,32 @@ export default defineNuxtConfig({ ssr: false, headers: { "Cache-Control": "no-cache, no-store, must-revalidate", + "Surrogate-Control": "no-store", }, }, "/checkout/**": { ssr: false, + headers: { + "Surrogate-Control": "no-store", + }, }, "/account": { ssr: false, + headers: { + "Surrogate-Control": "no-store", + }, }, "/account/**": { ssr: false, + headers: { + "Surrogate-Control": "no-store", + }, }, "/wishlist": { ssr: false, + headers: { + "Surrogate-Control": "no-store", + }, }, }, imports: {