diff --git a/tehriehlbudget-backend/Dockerfile b/tehriehlbudget-backend/Dockerfile index 167e045..bea005e 100644 --- a/tehriehlbudget-backend/Dockerfile +++ b/tehriehlbudget-backend/Dockerfile @@ -1,48 +1,51 @@ # syntax=docker/dockerfile:1.7 ARG NODE_VERSION=20 -FROM node:${NODE_VERSION}-alpine AS deps +FROM node:${NODE_VERSION}-alpine AS builder RUN apk add --no-cache libc6-compat openssl RUN corepack enable && corepack prepare pnpm@9 --activate WORKDIR /repo -# Force a hoisted (flat) node_modules layout for the image. The default pnpm -# isolated layout puts dependencies inside node_modules/.pnpm/@/ -# which makes `prisma generate`'s output (node_modules/.prisma/client/) live -# in a path that doesn't survive a multi-stage COPY. Hoisted gives us -# predictable /repo/node_modules/{@prisma,.prisma}/ paths. Local dev is -# unaffected — this .npmrc only exists inside the build context. +# Hoisted layout so `prisma generate` writes to predictable paths +# (/repo/node_modules/{@prisma,.prisma}/) instead of into .pnpm//. +# Local dev keeps the isolated layout — this .npmrc only exists in the +# build context. RUN echo "node-linker=hoisted" > /repo/.npmrc + COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ -COPY tehriehlbudget-backend/package.json tehriehlbudget-backend/ +COPY tehriehlbudget-backend/ tehriehlbudget-backend/ COPY tehriehlbudget-frontend/package.json tehriehlbudget-frontend/ + RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile --filter tehriehlbudget-backend... -FROM deps AS build -WORKDIR /repo -COPY tehriehlbudget-backend/ tehriehlbudget-backend/ RUN pnpm --filter tehriehlbudget-backend exec prisma generate RUN pnpm --filter tehriehlbudget-backend run build -FROM deps AS prod-deps -WORKDIR /repo -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ - pnpm install --frozen-lockfile --prod --filter tehriehlbudget-backend... +# pnpm deploy produces a self-contained prod-only bundle for the backend: +# only the dependencies the runtime actually needs, no frontend tooling, +# no devDeps. Drops vite, vitest, react, the whole NestJS CLI etc. that +# were leaking into the runtime via the workspace's hoisted node_modules. +RUN pnpm --filter tehriehlbudget-backend --prod deploy /deploy + +# Overlay the generated Prisma client. pnpm deploy copies the @prisma/client +# package as-is, but the co-located .prisma/client/ output directory isn't +# a package so it isn't always carried along. Copy it explicitly to be safe. +RUN cp -r /repo/node_modules/.prisma /deploy/node_modules/.prisma FROM node:${NODE_VERSION}-alpine AS runtime -RUN apk add --no-cache libc6-compat openssl tini +RUN apk add --no-cache libc6-compat openssl tini \ + # Remove the npm CLI that ships with node:alpine. We use pnpm via + # corepack at build time and `node dist/main` at runtime — npm is + # never invoked. Its bundled deps (cross-spawn, glob, minimatch, + # tar) are the source of 11 of the 12 HIGH/CRITICAL CVEs Trivy flags + # in the image, and they're all in code paths we never execute. + && rm -rf /usr/local/lib/node_modules/npm \ + /usr/local/bin/npm \ + /usr/local/bin/npx WORKDIR /app RUN addgroup -S nodeapp && adduser -S nodeapp -G nodeapp ENV NODE_ENV=production -COPY --from=build --chown=nodeapp:nodeapp /repo/tehriehlbudget-backend/dist ./dist -COPY --from=build --chown=nodeapp:nodeapp /repo/tehriehlbudget-backend/prisma ./prisma -COPY --from=build --chown=nodeapp:nodeapp /repo/tehriehlbudget-backend/package.json ./package.json -# With hoisted linker, all deps live at the workspace-root node_modules. -COPY --from=prod-deps --chown=nodeapp:nodeapp /repo/node_modules ./node_modules -# Overlay the generated Prisma client from the build stage. The prod-deps -# stage doesn't have the `prisma` CLI (devDep) so it can't generate. -COPY --from=build --chown=nodeapp:nodeapp /repo/node_modules/.prisma ./node_modules/.prisma -COPY --from=build --chown=nodeapp:nodeapp /repo/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder --chown=nodeapp:nodeapp /deploy/ ./ USER nodeapp EXPOSE 3000 ENTRYPOINT ["/sbin/tini", "--"]