The Prisma CLI ships Studio, which brings React and a graph-layout engine along with it — tens of megabytes for a browser tool this image will never run. Removing them takes the image from 705 MB to 649 MB. `effect` looks like part of the same bundle and is not: the CLI itself requires it, and removing it breaks `migrate deploy` outright. That was found by running the migration in the pruned image rather than by reading the dependency tree. Worth recording how the first attempt at this went wrong: the prune was tried inside a running container, where the files belong to root and the process runs as node, so every `rm` failed silently behind a `2>/dev/null`. The migration then passed against an untouched tree and looked like proof. The check only became a check once the prune happened at build time. The entrypoint migrates on every start, so an over-eager prune now fails loudly at first boot rather than quietly at the worst moment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
96 lines
4.1 KiB
Docker
96 lines
4.1 KiB
Docker
# syntax=docker/dockerfile:1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# deps — install once, cached on the lockfile alone
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS deps
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
RUN npm ci
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build — generate the Prisma client, then compile
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS build
|
|
WORKDIR /app
|
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY . .
|
|
# The client is generated into lib/generated and imported by the app, so it
|
|
# must exist before the build traces its dependencies.
|
|
RUN npx prisma generate && npm run build
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# migrator — the Prisma CLI, on its own, with its own dependency tree
|
|
#
|
|
# The CLI cannot simply be copied out of the build stage: it needs transitive
|
|
# dependencies that are scattered through a hoisted node_modules, and copying
|
|
# the whole tree would double the image. Installing it alone keeps it small and
|
|
# keeps it working. The version is read from our own package.json so it cannot
|
|
# drift from the client the application was built against. dotenv comes along
|
|
# because prisma.config.ts imports it; in a container the environment is
|
|
# already populated, but the file is shared with local development.
|
|
#
|
|
# The CLI ships Prisma Studio, which drags in React and a graph-layout engine
|
|
# this image will never run; those are removed here. `effect` looks like part
|
|
# of the same bundle but is not — the CLI itself requires it, and removing it
|
|
# breaks `migrate deploy` outright. The entrypoint runs a migration on every
|
|
# start, so an over-eager prune fails loudly at the first boot rather than
|
|
# quietly at the worst moment.
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS migrator
|
|
WORKDIR /cli
|
|
COPY package.json ./
|
|
RUN PRISMA_VERSION="$(node -p "require('./package.json').dependencies.prisma")" \
|
|
&& rm package.json \
|
|
&& npm init -y > /dev/null \
|
|
&& npm install --no-audit --no-fund --omit=dev "prisma@${PRISMA_VERSION}" dotenv \
|
|
&& rm -rf node_modules/react node_modules/react-dom node_modules/scheduler \
|
|
node_modules/elkjs node_modules/@electric-sql
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# runner — standalone output plus what the migrations need
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS runner
|
|
WORKDIR /app
|
|
|
|
ENV NODE_ENV=production \
|
|
NEXT_TELEMETRY_DISABLED=1 \
|
|
PORT=3010 \
|
|
HOSTNAME=0.0.0.0
|
|
|
|
# curl for the healthcheck, tzdata so Europe/Zurich is a real zone rather than
|
|
# a name Intl quietly falls back to UTC for.
|
|
RUN apk add --no-cache curl tzdata
|
|
|
|
COPY --from=build /app/.next/standalone ./
|
|
COPY --from=build /app/.next/static ./.next/static
|
|
COPY --from=build /app/public ./public
|
|
|
|
# `prisma migrate deploy` runs at startup, so the CLI, the schema, the
|
|
# migrations and the config travel with the image — all of them together under
|
|
# .migrator, which the entrypoint uses as its working directory.
|
|
#
|
|
# Keeping the config beside the CLI rather than at the application root is
|
|
# what makes the isolation hold: `prisma.config.ts` imports `prisma/config`
|
|
# and `dotenv`, and both resolve from the tree next to it. Splitting them
|
|
# would mean copying the CLI's dependencies into the application's own
|
|
# node_modules one failure at a time.
|
|
COPY --from=migrator /cli/node_modules ./.migrator/node_modules
|
|
COPY --from=build /app/prisma ./.migrator/prisma
|
|
COPY --from=build /app/prisma.config.ts ./.migrator/prisma.config.ts
|
|
|
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
|
|
# The image ships with a non-root user; use it.
|
|
USER node
|
|
|
|
EXPOSE 3010
|
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
CMD curl -fsS http://127.0.0.1:3010/api/health || exit 1
|
|
|
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
|
CMD ["node", "server.js"]
|