The previous attempts (--include=optional, then a separate npm install
fallback) failed because npm ci runs sharp's install script DURING
installation — and that script crashes ("Please add node-gyp to your
dependencies") before the next Dockerfile step gets to run.
Real fix: pin every sharp platform binary as an optionalDependency in
package.json. npm now records URL+hash for all of them in the lock
file regardless of which OS generated the lock. On any build host,
npm ci picks the matching binary via the os/cpu/libc filters in those
packages and silently skips the rest.
Pinned binaries (sharp 0.34.5):
- @img/sharp-linuxmusl-x64 (Alpine x64 — our VPS)
- @img/sharp-linuxmusl-arm64 (Alpine arm64)
- @img/sharp-linux-x64 (glibc x64)
- @img/sharp-linux-arm64 (glibc arm64)
- @img/sharp-darwin-arm64 (Apple Silicon dev)
- @img/sharp-darwin-x64 (Intel Mac dev)
Side benefit: simplifies the Dockerfile. Drops the secondary
`npm install --no-save --cpu=x64 --os=linux --libc=musl sharp` step
and the vips-dev system package (no source compilation needed when
the prebuilt binary is guaranteed present). The runner stage still
needs `vips` runtime, that stays.
The Docker build on the VPS failed because:
npm error Missing: @swc/helpers@0.5.21 from lock file
Cause: Next.js 16.1.6 declares @swc/helpers both as a direct dep at
0.5.15 AND as a peer dep ">=0.5.17". npm 11 (my local) accepts the
0.5.15 install and considers the peer satisfied. npm 10.9.7 (the
Alpine container in the VPS) is stricter — it tries to resolve the
peer to its own version and demands 0.5.21, which our lock file
didn't have.
Fix:
- Add @swc/helpers ^0.5.17 as a direct dependency so both npm 10 and
npm 11 pick the same up-to-date version (resolved to 0.5.21).
- Regenerate package-lock.json from a clean install so it matches.
`npm ci` now succeeds in the docker-compose build.
Two production-grade hardening additions and one cost optimisation.
FLUXAI AUTONOMY RESTORED (api/chat)
- Brings back the multi-step agentic flow that the system prompt was
always designed for. The "temporarily removed maxSteps" comment is
gone — replaced with the AI SDK 6 equivalent stopWhen: stepCountIs(5).
- Cap at 5 chained tool calls per turn bounds latency + LLM cost.
- maxDuration raised 30s → 60s to absorb tool-chain runs.
- Result: one user prompt now triggers, e.g. search_installations →
energy_savings_calculator → show_case_study → schedule_consultation
in a single turn — exactly the SPIN methodology in the prompt.
RATE LIMITING (src/lib/rateLimit.ts + api/chat)
- Token-bucket per IP: 30 messages burst, sustained 30/minute. Trips
to 429 with Retry-After + X-RateLimit-Remaining headers when abused.
- IP extracted from x-forwarded-for (Nginx already passes this).
- In-memory Map with 10-min GC of stale buckets — no Redis dep.
If we scale to multiple replicas later, swap the Map for Upstash.
- Protects the OpenAI quota from someone hammering the chat endpoint.
IMAGE PIPELINE (src/lib/imageOptimizer.ts)
- sharp-based optimizer: auto-orient (EXIF), cap at 2560px long side,
re-encode WebP@85, content-hash filename. Re-uploads with same
content reuse the same hash; new content gets a new URL — perfect
cache invalidation without header tricks.
- Opt-in via optimize=1 form/query param on /api/assets POST.
- Hero CMS and Site Settings uploads turn it on automatically (those
are user-facing brand assets where compression matters most).
- App/news/parts uploads remain untouched (editors may be uploading
CAD drawings, datasheets, etc. that shouldn't be transcoded).
- Falls back gracefully to a no-op for unsupported formats (SVG, GIF,
videos, anything sharp can't decode) so it never breaks an upload.
DOCKERFILE
- Adds vips/vips-dev for sharp on Alpine + --include=optional so the
@img/sharp-linuxmusl-x64 prebuilt is downloaded
- Explicitly copies node_modules/sharp + node_modules/@img to the
runner stage (Next.js trace can miss conditional deps).
NO DB SCHEMA CHANGES.