Run Prisma 7 migrations inside a multi-stage Docker production image. Use when: (1) `prisma migrate deploy` crashes with "Cannot resolve environment variable: DATABASE_URL" or "cannot find module" at container startup, (2) building a production Docker image that needs to run migrations before starting the server, (3) you copied only node_modules/prisma + node_modules/@prisma but the Prisma 7 CLI still fails with MODULE_NOT_FOUND at runtime, (4) Yarn 4 node-modules linker .bin/prisma symlink breaks after Docker COPY. Covers prisma.config.ts (Prisma 7), full node_modules copy requirement, and symlink recreation pattern.
Prisma 7 changed how datasource URLs are configured and significantly expanded the CLI's runtime dependency closure. Two things that worked in Prisma 5/6 break silently in Prisma 7:
url = env("DATABASE_URL") in schema.prisma is no longer valid — Prisma 7 moves
this to prisma.config.ts via defineConfig().node_modules/prisma + node_modules/@prisma is not enough
— Prisma 7 CLI loads additional packages at runtime that aren't in those scopes.Additionally, Yarn 4 node-modules linker creates .bin/prisma as a symlink. Docker COPY
dereferences symlinks, silently converting the symlink to a regular file at the wrong location,
breaking WASM engine loading.
nodeLinker: node-modulesPrismaConfigEnvError: Cannot resolve environment variable: DATABASE_URLError: Cannot find module '@prisma/config'Error: Cannot find module 'effect'Error: WASM file not found or similar engine loading errorsprisma validate passes locally but fails in the containerschema.prisma no longer holds the datasource URLIn Prisma 7, datasource db { url = env("DATABASE_URL") } was removed from schema.prisma.
The URL now lives in prisma.config.ts:
// packages/server/prisma.config.ts
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: { path: 'prisma/migrations' },
datasource: { url: env('DATABASE_URL') },
})
Prisma 7 CLI requires at runtime (beyond prisma and @prisma):
effectfast-checkpure-rand@prisma/configSelective copying of node_modules/prisma + node_modules/@prisma misses these.
.bin/ symlinksIn Yarn 4 node-modules linker, .bin/prisma is a symlink to
../prisma/build/index.js (relative path). Docker COPY follows the symlink and
copies the file content to the destination, preserving __dirname as the source
directory — not the destination. This breaks WASM loading which is relative to __dirname.
prisma.config.ts from the build stageCOPY --from=build-server /app/packages/server/prisma ./prisma
COPY --from=build-server /app/packages/server/prisma.config.ts ./prisma.config.ts
node_modules (until a deps-prod stage is available)# Prisma 7 CLI requires its full transitive dependency closure at runtime.
# TODO: Introduce a deps-prod stage (yarn workspaces focus server --production
# after adding @yarnpkg/plugin-workspace-tools) to reduce image size.
COPY --from=deps /app/node_modules ./node_modules
.bin/prisma symlink after COPY# Yarn 4 .bin/prisma is a symlink that Docker COPY dereferences. Recreate it so
# __dirname resolves to prisma/build/ for correct WASM engine loading.
RUN ln -sf /app/node_modules/prisma/build/index.js /app/node_modules/.bin/prisma
node_modules/.bin to PATH and update CMDENV PATH="/app/node_modules/.bin:$PATH"
CMD ["sh", "-c", "prisma migrate deploy && node server/index.mjs"]
prisma from devDependencies to dependenciesIn packages/server/package.json, move prisma to dependencies so the runtime
requirement is explicit and won't silently break if the install strategy changes:
{
"dependencies": {
"prisma": "7.x.x"
}
}
# ── Production ───────────────────────────────────────────────────
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=build-server /app/packages/server/dist ./server
COPY --from=build-web /app/packages/web/dist ./web
# Prisma: schema + migrations + config (Prisma 7 uses prisma.config.ts for datasource URL)
COPY --from=build-server /app/packages/server/prisma ./prisma
COPY --from=build-server /app/packages/server/prisma.config.ts ./prisma.config.ts
# Prisma 7 CLI requires its full transitive dependency closure at runtime.
# TODO: Introduce a deps-prod stage (yarn workspaces focus server --production
# after adding @yarnpkg/plugin-workspace-tools) to reduce image size.
COPY --from=deps /app/node_modules ./node_modules
# Yarn 4 .bin/prisma is a symlink that Docker COPY dereferences. Recreate it so
# __dirname resolves to prisma/build/ for correct WASM engine loading.
RUN ln -sf /app/node_modules/prisma/build/index.js /app/node_modules/.bin/prisma
ENV PATH="/app/node_modules/.bin:$PATH"
ENV NODE_ENV=production
EXPOSE 4000
CMD ["sh", "-c", "prisma migrate deploy && node server/index.mjs"]
Run the production image without DATABASE_URL — it should fail with a clear
Prisma config error (not a Node.js module error):
docker build -t test:local .
docker run --rm test:local
# Expected: PrismaConfigEnvError: Cannot resolve environment variable: DATABASE_URL
# NOT: Error: Cannot find module '...'
If you see Cannot find module, the dependency closure is still incomplete.
The full node_modules copy significantly inflates the image. The correct long-term fix:
@yarnpkg/plugin-workspace-tools to Yarn pluginsdeps-prod stage: RUN yarn workspaces focus server --productionUntil then, the full copy is the reliable approach.
.ts config files are executed by the CLI's internal TypeScript loader
— tsx is NOT required in the production imagerestart: unless-stopped Docker compose policy will cause a crash-loop if
prisma migrate deploy fails — set a start_period on your healthcheck and
monitor logsprisma should be in dependencies, not devDependencies, since it runs at
container startup