FROM node:22-alpine AS base # Install pnpm as a real binary via npm (not corepack shim) so runtime # invocations of `pnpm` work without DNS access to registry.npmjs.org. # The corepack shim delegates to corepack, which re-validates against # npmjs.org on first use — that fails in air-gapped UAT seed/migrate/reset # Jobs. GRO-1983 / GRO-1889 / GRO-1909 / GRO-1981 / GRO-1985. RUN npm install -g pnpm@9.15.4 # Belt-and-braces: disable Corepack's download fallback so that even if a # Corepack shim is somehow invoked at runtime, it will not try to fetch # pnpm from registry.npmjs.org. Belt for the real-binary trousers. GRO-1985. ENV COREPACK_ENABLE_DOWNLOAD_FALLBACK=0 WORKDIR /app # Install deps FROM base AS deps COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ COPY packages/db/package.json packages/db/ COPY packages/types/package.json packages/types/ RUN pnpm install --frozen-lockfile # Build FROM deps AS builder COPY packages/ packages/ COPY src/ src/ COPY tsconfig.json ./ RUN pnpm --filter @groombook/types build && \ pnpm --filter @groombook/db build && \ pnpm build # Runtime FROM node:22-alpine AS runner RUN npm install -g pnpm@9.15.4 # Same defence-in-depth as base: no Corepack fallback. GRO-1985. ENV COREPACK_ENABLE_DOWNLOAD_FALLBACK=0 WORKDIR /app ENV NODE_ENV=production COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ COPY --from=builder /app/package.json ./ COPY --from=builder /app/dist dist/ COPY --from=builder /app/packages/db/package.json packages/db/ COPY --from=builder /app/packages/db/dist packages/db/dist COPY --from=builder /app/packages/types/package.json packages/types/ COPY --from=builder /app/packages/types/dist packages/types/dist RUN pnpm install --frozen-lockfile --prod EXPOSE 3000 RUN apk add --no-cache curl HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:3000/health || exit 1 CMD ["node", "dist/index.js"] # Migrate stage — runs drizzle-kit migrate against the database FROM builder AS migrate # pnpm needs a writable HOME for any config/state it writes. With # readOnlyRootFilesystem: true and runAsUser: 1000, /home/node is read-only. # The job pods mount a writable emptyDir at /tmp; point HOME there. GRO-1985. ENV HOME=/tmp CMD ["pnpm", "--filter", "@groombook/db", "migrate"] # Seed stage — populates the database with test data FROM builder AS seed ENV HOME=/tmp CMD ["pnpm", "--filter", "@groombook/db", "seed"] # Reset stage — drops all tables, re-runs migrations, and re-seeds FROM builder AS reset ENV HOME=/tmp CMD ["pnpm", "--filter", "@groombook/db", "reset"]