Files
pn-new-crm/Dockerfile
Matt 2315b58764
Some checks failed
Build & Push Docker Images / lint (push) Successful in 2m51s
Build & Push Docker Images / build-and-push (push) Failing after 5m16s
fix(docker): bundle socket.io transitive deps into crm-app runner
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>
2026-06-02 23:03:57 +02:00

58 lines
2.6 KiB
Docker

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate
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
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NODE_ENV=production in the builder makes `next build` and any code
# branching on isProd deterministic (build-auditor M9). Without this,
# CSP and other prod-only paths would compile under whatever NODE_ENV
# the host carried in.
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV SKIP_ENV_VALIDATION=1
RUN pnpm build
# Stage 3: Production runner
FROM node:20-alpine AS runner
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
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
# 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 \
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-3000}/api/health || exit 1
CMD ["node", "server-custom.js"]