From 4d615d694e8bc93d54eae7214e75905014f0998d Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Sat, 9 May 2026 08:19:35 -1000 Subject: [PATCH] fix(registry): use pnpm deploy for monorepo-aware Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #100 switched the registry's `@nimblebrain/mpak-schemas` dep from `^0.2.0` (published) to `workspace:*` so schemas changes are immediately consumable without a publish-bump dance — matching the convention every other package in the workspace already uses (`cli`, `sdk-typescript`, `web`). The Dockerfile, however, was written single-package and only copied `apps/registry/`, never any workspace dep source. Build then errored across every file importing schemas: src/routes/auth.ts(3,53): error TS2307: Cannot find module '@nimblebrain/mpak-schemas' or its corresponding type declarations. ... (15 more) ## The fix: `pnpm deploy` `pnpm deploy --filter= --prod ` is pnpm's purpose-built solution for "containerize a workspace package": it walks the full dependency closure (workspace + npm), builds a flattened deployable directory, and resolves all `workspace:*` links by bundling each workspace package's compiled `dist/` inline. Single COPY in the runtime stage; future workspace deps require no Dockerfile change. Builder stage: - `COPY . .` brings the whole monorepo in (kept lean by .dockerignore). - `pnpm install --frozen-lockfile` resolves all workspace links. - `pnpm --filter ... exec prisma generate` writes the generated client into the pnpm hoisted store. - `pnpm --filter @nimblebrain/mpak-registry... build` builds the registry AND every transitive workspace dep (`...` filter syntax). - `pnpm deploy --filter=@nimblebrain/mpak-registry --prod /deploy` flattens registry + prod deps + workspace closures into /deploy. Production stage: - One `COPY --from=builder /deploy ./` brings everything the runtime needs — registry dist, schemas dist, every npm prod dep. - The Prisma generated client is the one exception: its sibling `.prisma/` folder lives in the pnpm hoisted store and isn't carried by `pnpm deploy`. Two explicit COPY lines preserved from the prior Dockerfile handle that special case. `.dockerignore` (new) keeps the build context small — excludes every package's `node_modules`, `dist`, `.astro`, `.turbo`, `.git`, `.env*`, test outputs, editor state. ## Verified locally - `docker build -f apps/registry/Dockerfile -t mpak-registry:test .` → succeeds end-to-end (was failing on the original tsc step). - `docker run mpak-registry:test` → boots: Prisma initializes, every module loads, Fastify listens on :3200. - `node -e "import('@nimblebrain/mpak-schemas').then(...)"` inside the image → schemas resolves, `ServerDetailSchema` is an object. ## Why not the simpler `COPY packages/schemas/...` patch Considered first, rejected because it's per-dep — every future workspace dep added to the registry would need another COPY pair in both stages. `pnpm deploy` is what pnpm's monorepo-Docker docs recommend and scales to N workspace deps with no Dockerfile churn. --- .dockerignore | 51 +++++++++++++++++++++++++++++ apps/registry/Dockerfile | 69 ++++++++++++++++++++-------------------- 2 files changed, 85 insertions(+), 35 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0bb3d96 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# Keep the docker build context small. The Dockerfile uses `COPY . .` to +# pull the whole monorepo in (so `pnpm deploy` can walk the workspace +# graph); this file makes sure that doesn't include build artifacts, +# installed deps, or local-only state. + +# Installed deps — pnpm reinstalls inside the image. +**/node_modules +.pnpm-store + +# Build outputs — recreated by `pnpm install` + per-package build. +**/dist +**/build +**/*.tsbuildinfo +**/.astro +**/.turbo +**/.next + +# VCS + workspace state. +.git +.github +.claude + +# Local environment + secrets — must never enter the image. +.env +.env.* +**/.env +**/.env.* + +# Test + coverage outputs. +**/coverage +**/.nyc_output +**/.pytest_cache +**/.ruff_cache + +# Editor / OS noise. +.vscode +.idea +.DS_Store +**/*.swp +**/*.swo + +# Misc large or sensitive paths that don't need to ship to the registry +# image specifically. +apps/registry/data +apps/web/dist +apps/web/.astro +apps/docs/dist +apps/docs/.astro + +# Existing implementation-tracking dirs. +.tasks diff --git a/apps/registry/Dockerfile b/apps/registry/Dockerfile index 7d67828..30220fd 100644 --- a/apps/registry/Dockerfile +++ b/apps/registry/Dockerfile @@ -1,54 +1,53 @@ -# Build stage +# Build stage — full monorepo install + build the registry + flatten its +# closure into /deploy via `pnpm deploy`. Workspace deps (e.g. the +# `workspace:*` link to @nimblebrain/mpak-schemas) and any future +# additions are handled automatically — pnpm walks the dependency +# graph and copies what's needed. FROM node:22-alpine AS builder RUN corepack enable pnpm WORKDIR /app -# Copy workspace config -COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./ +# Whole monorepo lands here. `.dockerignore` keeps the context small +# (no node_modules, dist, .git, etc.). +COPY . . -# Copy the registry app -COPY apps/registry/package.json apps/registry/tsconfig.json apps/registry/prisma.config.ts ./apps/registry/ -COPY apps/registry/prisma/ ./apps/registry/prisma/ -COPY apps/registry/src/ ./apps/registry/src/ - -# Install dependencies +# One install resolves every workspace package's deps + workspace links. RUN pnpm install --frozen-lockfile -# Generate Prisma client -RUN cd apps/registry && npx prisma generate +# Generate the Prisma client into the pnpm hoisted store. tsc downstream +# consumes its types; the runtime stage copies the generated artifacts +# explicitly (see below). +RUN pnpm --filter @nimblebrain/mpak-registry exec prisma generate -# Build -RUN cd apps/registry && pnpm build +# Build registry + every transitive workspace dep (schemas, etc.). +RUN pnpm --filter @nimblebrain/mpak-registry... build -# Production stage -FROM node:22-alpine AS production +# Flatten registry + production deps into /deploy. pnpm walks the +# dependency closure and copies every workspace package's `dist/` plus +# every external prod dep — single-stage output, no per-dep COPY pairs +# in the runtime stage as the workspace grows. +RUN pnpm deploy --filter=@nimblebrain/mpak-registry --prod /deploy -RUN corepack enable pnpm +# Production stage — minimal runtime image. +FROM node:22-alpine AS production WORKDIR /app -# Copy workspace config -COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ - -# Copy the registry app package.json -COPY apps/registry/package.json ./apps/registry/ - -# Install production dependencies only -RUN pnpm install --frozen-lockfile --prod - -# Copy Prisma schema and generated client (Prisma 7 generates to hoisted node_modules) -COPY --from=builder /app/apps/registry/prisma/ ./apps/registry/prisma/ -COPY --from=builder /app/node_modules/.pnpm/@prisma+client*/node_modules/@prisma/client/ ./node_modules/@prisma/client/ +# Everything the registry needs at runtime, prepared by `pnpm deploy`: +# its own dist/, its production deps (including @prisma/client base), +# and every workspace dep's compiled output. +COPY --from=builder /deploy ./ + +# Prisma's generated client lands in the pnpm hoisted store at +# `node_modules/.pnpm/@prisma+client*/node_modules/{.prisma,@prisma/client}/` +# during `prisma generate`. `pnpm deploy` carries the @prisma/client +# package itself but not the sibling .prisma folder where the +# *generated* client and engine binaries live. Copy both explicitly so +# the runtime can find them at the canonical lookup paths. COPY --from=builder /app/node_modules/.pnpm/@prisma+client*/node_modules/.prisma/ ./node_modules/.prisma/ -COPY --from=builder /app/node_modules/.pnpm/@prisma+client-runtime-utils*/node_modules/@prisma/client-runtime-utils/ ./node_modules/@prisma/client-runtime-utils/ -COPY --from=builder /app/node_modules/.pnpm/@prisma+client-runtime-utils*/node_modules/@prisma/client-runtime-utils/ ./node_modules/@prisma/client-runtime-utils/ - -# Copy built output -COPY --from=builder /app/apps/registry/dist/ ./apps/registry/dist/ - -WORKDIR /app/apps/registry +COPY --from=builder /app/node_modules/.pnpm/@prisma+client*/node_modules/@prisma/client/ ./node_modules/@prisma/client/ # Environment ENV NODE_ENV=production