# Debian-based (not alpine) on purpose: avoids musl-vs-glibc native binding # surprises for argon2/Prisma's engine binaries. Same base image family for # build and runtime stages, so "native" binaries built in `build` are # guaranteed compatible with `runtime`. FROM node:22-slim AS base # Prisma's query engine needs OpenSSL to be present to detect the right # binary target; without it, it silently defaults to a guess (openssl-1.1.x) # that may not match what's actually on the image and fail at runtime. RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/* RUN corepack enable WORKDIR /repo # Single image serving both the API and the built frontend (apps/web) — one # process, one container, no separate nginx/static host. Builds both so the # runtime stage below can copy each app's build output independently. FROM base AS build # `pnpm install` (below) installs every workspace's dependencies, including # apps/web's `cypress` devDependency — this image never runs it, so skip its # (large, Electron-bundled) binary download: saves build time/bandwidth and # removes a network dependency on Cypress's CDN from every production build. # The `cypress` npm *package* itself still ends up in `node_modules` (it's a # real lockfile entry) and gets copied into `runtime` below — a deliberate # tradeoff. Tried to prune it out too (`pnpm prune --prod`, then a full # wipe-and-reinstall with `--prod`), but in this pnpm workspace both proved # actively unsafe rather than just ineffective: `prune` doesn't cascade into # sibling workspace projects' `node_modules` at all, and even a *scoped* # `pnpm --filter web prune --prod` emptied out apps/api's entire # `node_modules` as a side effect; the wipe-and-reinstall variant silently # dropped argon2's compiled native binding (no postinstall re-run to rebuild # it), which only surfaced as a crash *after* deploy # (`Cannot find module '.../argon2.node'`) — not something to risk on the # real production image for what's ~10MB of inert JS once the binary # download above is already skipped. ENV CYPRESS_INSTALL_BINARY=0 COPY . . RUN pnpm install --frozen-lockfile RUN pnpm --filter api build RUN pnpm --filter web build # Copies the monorepo structure as-is (not a flattened single package) so # pnpm's symlinked node_modules (root node_modules/.pnpm <- apps/api/node_modules) # stay valid — paths must match exactly between build and runtime stages. FROM base AS runtime ENV NODE_ENV=production # Tells the API where to find the built frontend — see FRONTEND_DIST_DIR's # doc comment in apps/api/src/config/env.ts. ENV FRONTEND_DIST_DIR=/repo/apps/web/dist COPY --from=build /repo/node_modules ./node_modules COPY --from=build /repo/package.json ./package.json COPY --from=build /repo/pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --from=build /repo/packages/shared ./packages/shared COPY --from=build /repo/packages/error-tools ./packages/error-tools COPY --from=build /repo/packages/express-tools ./packages/express-tools COPY --from=build /repo/packages/date-tools ./packages/date-tools COPY --from=build /repo/apps/api/node_modules ./apps/api/node_modules COPY --from=build /repo/apps/api/dist ./apps/api/dist COPY --from=build /repo/apps/api/prisma ./apps/api/prisma COPY --from=build /repo/apps/api/package.json ./apps/api/package.json COPY --from=build /repo/apps/web/dist ./apps/web/dist WORKDIR /repo/apps/api EXPOSE 3000 # Applies pending migrations, then seeds the reference data (Diet/Category/ # Allergy — see src/scripts/seed-runtime.ts) before starting. Both steps are # safe to repeat on every container start: migrate deploy only applies # pending migrations, and the seed upserts by unique name. CMD ["sh", "-c", "node_modules/.bin/prisma migrate deploy && node dist/scripts/seed-runtime.js && node dist/server.js"]