# Multi-stage build. NextChair is the internal project/image name only — # see docs/phase-8-summary.md's white-label section for why that's fine # here (Docker/package names are explicitly "internal and acceptable") # even though the running application never shows that name to a user. # # Prisma 7 uses driver adapters (@prisma/adapter-mariadb), not a native # Rust query-engine binary — the generated client is pure JS, so this # image needs no special native-binary handling the way older Prisma # versions on Alpine sometimes did. # # `output: "standalone"` in next.config.ts was deliberately NOT enabled: # this image runs `prisma migrate deploy` (and, for the optional seed # service, `tsx prisma/seed.ts`) at container start, both of which need # the full `prisma`/`tsx` CLIs and their dependencies present — a pruned # standalone bundle would need those re-added anyway, so a single # `npm ci` (full dependencies, including the small set of dev tools this # image actually runs) is simpler and no less correct. See # docs/phase-8-summary.md, "Docker image quality" for this trade-off. FROM node:22-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # `npm run build` runs `prisma generate` itself (see package.json — needed # so a plain Vercel/`npm run build` deploy generates the client too, not # just this Docker path, which used to have its own separate `prisma # generate` step here). # `next build`'s page-data collection step imports every route module # (including /api/health) to inspect it — that pulls in # src/lib/db/prisma.ts, which requires DATABASE_HOST/NAME/USER/PASSWORD # just to construct its connection-pool config object, even though nothing # actually connects at build time. These placeholders only exist to # satisfy that check; the real values are supplied at container runtime # via env_file (see docker-compose.local.yaml) and these are never used to # connect to anything or baked into a layer that matters. ENV DATABASE_HOST="localhost" ENV DATABASE_PORT="3306" ENV DATABASE_NAME="build" ENV DATABASE_USER="build" ENV DATABASE_PASSWORD="build" RUN npm run build FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production # Non-root — see the brief's "Docker image quality: non-root user." RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextchair COPY --from=builder --chown=nextchair:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=nextchair:nodejs /app/.next ./.next COPY --from=builder --chown=nextchair:nodejs /app/public ./public COPY --from=builder --chown=nextchair:nodejs /app/generated ./generated COPY --from=builder --chown=nextchair:nodejs /app/prisma ./prisma COPY --from=builder --chown=nextchair:nodejs /app/src ./src COPY --from=builder --chown=nextchair:nodejs /app/scripts ./scripts COPY --from=builder --chown=nextchair:nodejs /app/package.json ./package.json COPY --from=builder --chown=nextchair:nodejs /app/prisma.config.ts ./prisma.config.ts COPY --from=builder --chown=nextchair:nodejs /app/next.config.ts ./next.config.ts COPY --from=builder --chown=nextchair:nodejs /app/tsconfig.json ./tsconfig.json COPY --chown=nextchair:nodejs docker/entrypoint.sh ./docker/entrypoint.sh RUN chmod +x ./docker/entrypoint.sh # Media Manager uploads (Phase 10) — storage/media (NOT public/) is a # named volume mount point (see docker-compose.local.yaml). Served by # src/app/api/media/[...path]/route.ts, not Next's static handling for # public/ — verified against a real production build that runtime-added # public/ files 404 (Next resolves public assets at build time, not from # a live filesystem check). Creating this here, owned by the non-root # user, before the volume first attaches means Docker initializes the # volume with this ownership rather than root's. RUN mkdir -p ./storage/media && chown -R nextchair:nodejs ./storage USER nextchair EXPOSE 3000 ENV PORT=3000 ENTRYPOINT ["./docker/entrypoint.sh"]