fix(docker): bundle socket.io transitive deps into crm-app runner
Some checks failed
Build & Push Docker Images / lint (push) Successful in 2m51s
Build & Push Docker Images / build-and-push (push) Failing after 5m16s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 23:03:57 +02:00
parent 15a139e86f
commit 2315b58764

View File

@@ -5,6 +5,17 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod=false 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 # Stage 2: Build the application
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate 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/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/dist/server.js ./server-custom.js 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 # The custom socket.io server (server-custom.js, built with esbuild
# server (server-custom.js) requires them at runtime, but the Next # --packages=external) resolves socket.io and its FULL transitive closure
# tracer has no reason to include them in .next/standalone since no # (engine.io → accepts/ws/cors, @socket.io/redis-adapter, ...) from
# Next route imports the socket server. (build-auditor C3) # node_modules at runtime. The Next tracer omits these from
COPY --from=deps --chown=nextjs:nodejs /app/node_modules/socket.io ./node_modules/socket.io # .next/standalone because no Next route imports the socket server, and
COPY --from=deps --chown=nextjs:nodejs /app/node_modules/@socket.io ./node_modules/@socket.io # 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 USER nextjs
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \