From 2315b587649106b302ce17218dad1b8d307d2d00 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 23:03:57 +0200 Subject: [PATCH] fix(docker): bundle socket.io transitive deps into crm-app runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crm-app image cherry-copied only socket.io + @socket.io into the runner's node_modules, omitting their transitive closure (engine.io → accepts/ws/cors, ...). server-custom.js is built with esbuild --packages=external, so it require()s those at runtime and crashed with MODULE_NOT_FOUND 'accepts' on first boot. CI reported success because it only builds+pushes — the image runtime was never exercised. Add a hoisted (symlink-free) prod-deps stage and overlay the complete prod dependency tree onto the traced standalone subset. Hoisted layout makes the Docker COPY faithful (pnpm's default symlinked layout dereferences and breaks resolution). Validated locally: accepts, engine.io, socket.io, @socket.io/redis-adapter, ws, cors all resolve. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9af9f1c8..27e41342 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,17 @@ WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile --prod=false +# Stage 1b: Production dependency tree in a flat (hoisted) node_modules. +# Hoisted = symlink-free, so a Docker COPY into the runner is faithful +# (copying pnpm's default symlinked layout dereferences and breaks +# transitive resolution); complete = the custom socket.io server's deps +# (engine.io, accepts, ws, ...) all resolve at runtime. +FROM node:20-alpine AS prod-deps +RUN corepack enable && corepack prepare pnpm@10.33.2 --activate +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN echo "node-linker=hoisted" > .npmrc && pnpm install --frozen-lockfile --prod + # Stage 2: Build the application FROM node:20-alpine AS builder RUN corepack enable && corepack prepare pnpm@10.33.2 --activate @@ -30,12 +41,15 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/dist/server.js ./server-custom.js -# Pin socket.io + @socket.io/redis-adapter into the runner — the custom -# server (server-custom.js) requires them at runtime, but the Next -# tracer has no reason to include them in .next/standalone since no -# Next route imports the socket server. (build-auditor C3) -COPY --from=deps --chown=nextjs:nodejs /app/node_modules/socket.io ./node_modules/socket.io -COPY --from=deps --chown=nextjs:nodejs /app/node_modules/@socket.io ./node_modules/@socket.io +# The custom socket.io server (server-custom.js, built with esbuild +# --packages=external) resolves socket.io and its FULL transitive closure +# (engine.io → accepts/ws/cors, @socket.io/redis-adapter, ...) from +# node_modules at runtime. The Next tracer omits these from +# .next/standalone because no Next route imports the socket server, and +# cherry-copying just socket.io/ leaves its deps unresolved +# (MODULE_NOT_FOUND 'accepts'). Overlay the complete prod dependency tree +# (flat/hoisted layout → symlink-safe copy) on top of the traced subset. +COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules USER nextjs EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \