Docker and Prisma on Windows: Lessons Learned
On March 1, 2026, Docker deployment on Vandoko broke in six distinct ways. I fixed all six in a single day. Here is what broke and how I fixed each one.
The Setup
Vandoko runs a NestJS backend and Next.js frontend in a pnpm monorepo with Turbo. The Docker setup uses a multi-stage build: a builder stage that installs dependencies and compiles TypeScript, and a runner stage that runs the compiled output.
The deployment target is Railway. The local development environment is Windows 11.
Fix 1: Prisma Client Not Copying to Runner
The first thing that broke was Prisma. The build stage ran prisma generate and compiled the application. The runner stage started and immediately failed because the Prisma client was not there.
The issue: prisma generate writes the generated client to node_modules/@prisma/client. When you copy the built application to the runner stage, you need to explicitly copy that directory. It does not come along automatically.
# In the runner stage
COPY --from=builder /app/node_modules/.prisma /app/node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma /app/node_modules/@prismaThat fixed it.
Fix 2: pnpm Hoisted node_modules
pnpm's default behavior puts packages in a content-addressable store and creates symlinks. That works on Linux. On Windows it creates problems, and in a Docker container built on a Windows host it creates a different set of problems.
I switched to hoisted mode, which puts packages directly in node_modules like npm does. That required a .npmrc file:
node-linker=hoisted
Hoisted mode resolved the module resolution failures in the container.
Fix 3: Missing .npmrc in the Docker Image
The .npmrc file existed locally but was not being copied into the Docker image. The Dockerfile had a .dockerignore that was excluding it.
Fix: add .npmrc to the build stage copy before running pnpm install.
COPY .npmrc ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfileOrder matters. The .npmrc has to be present before the install runs.
Fix 4: pnpm Frozen Lockfile on Vercel
A separate but related issue: Vercel builds were failing with frozen-lockfile errors after the switch to hoisted mode. The pnpm-lock.yaml had changed locally and the Vercel cache was stale.
Fix: clear the Vercel build cache and redeploy. The lockfile regenerated correctly on the next build.
Fix 5: next-mdx-remote Breaking the Build
next-mdx-remote was breaking the Vercel build for the Next.js frontend. The error was a webpack module resolution failure in production.
Fix: replaced next-mdx-remote with react-markdown plus a webpack fallback config in next.config.ts. The fallback handles the node modules that next-mdx-remote was pulling in that webpack cannot bundle for the browser.
Fix 6: Workspace Package Resolution
The monorepo has a shared package that both the API and web apps depend on. After the hoisted mode switch, the workspace package was not resolving correctly in the Docker build context.
Fix: explicit workspace package copy in the Dockerfile before the install step, and a pnpm-workspace.yaml that the Docker build stage could see.
The Windows Factor
The pnpm hoisting issue is specifically a Windows problem. On macOS or Linux, pnpm's default symlink mode works fine in Docker. On Windows, the symlinks do not translate correctly into the container filesystem.
The .npmrc with node-linker=hoisted is now in every project that uses pnpm on Windows. It is a one-line fix but you have to know to look for it.
The rm -rf restriction is another Windows-specific issue that hit me on a CMS migration project. Windows permission system blocks directory deletion through some tools. The workaround is individual file deletes or using the shell's built-in remove rather than the Unix command.
After the Six Fixes
The deployment held. No further Docker issues on Vandoko after March 1. The fixes are documented in the project vault and have informed how I set up Docker for subsequent projects.
The lesson is not that Docker is hard. The lesson is that the gap between local development and production deployment is where bugs live, and the fastest way to close that gap is to run the Docker build locally before pushing.