# PIG — production image. # # Multi-stage so the runtime image carries no build toolchain and no source # maps. The front end is built into the API's static directory and served from # the same origin, which matters for more than tidiness: auth sessions are # per-origin, so splitting the app across two hostnames turns sign-in into a # redirect loop that looks like a broken deployment. FROM node:22-alpine AS build WORKDIR /app # Corepack installs the exact pnpm pinned by `packageManager`, so the image # builds with the same version as CI and as a developer's laptop. # # Both variables are load-bearing in a container build, and neither is # optional: # - the download prompt cannot be answered by a non-interactive build; # - CI=true is what stops pnpm asking for confirmation before it touches a # modules directory it considers stale. Without it the build fails with # ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY, which reads like a bug but is # pnpm correctly refusing to delete files nobody confirmed. ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ENV CI=true RUN corepack enable # Manifests and the lockfile first, so a dependency install is cached across # source-only edits. pnpm needs every workspace manifest present to resolve the # graph, hence the file-by-file copy rather than `COPY . .`. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY packages/core/package.json packages/core/ COPY packages/db/package.json packages/db/ COPY packages/prime/package.json packages/prime/ COPY apps/api/package.json apps/api/ COPY apps/web/package.json apps/web/ COPY apps/mcp/package.json apps/mcp/ COPY apps/cli/package.json apps/cli/ COPY apps/piggy/package.json apps/piggy/ RUN pnpm install --frozen-lockfile COPY . . # Typecheck as a build gate. A deploy that does not compile should fail here, # loudly, rather than at runtime in front of a user. RUN pnpm run typecheck RUN pnpm -F @pig/web run build # ---------------------------------------------------------------- runtime FROM node:22-alpine AS runtime WORKDIR /app ENV NODE_ENV=production ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ENV CI=true RUN corepack enable # Install production dependencies only. The server runs TypeScript directly, so # tsx is declared in `dependencies` rather than `devDependencies` — it is # genuinely needed at runtime, and pretending otherwise meant the old image had # to reinstall it by hand after pruning. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY packages/core/package.json packages/core/ COPY packages/db/package.json packages/db/ COPY packages/prime/package.json packages/prime/ COPY apps/api/package.json apps/api/ COPY apps/mcp/package.json apps/mcp/ COPY apps/cli/package.json apps/cli/ COPY apps/piggy/package.json apps/piggy/ # apps/web is a build-time workspace only; its manifest is still required for # the lockfile to resolve, but none of its dependencies are installed here. COPY apps/web/package.json apps/web/ RUN pnpm install --frozen-lockfile --prod --ignore-scripts COPY packages ./packages COPY apps/api ./apps/api COPY apps/mcp ./apps/mcp COPY apps/cli ./apps/cli COPY apps/piggy ./apps/piggy COPY --from=build /app/apps/web/dist ./apps/web/dist # Run unprivileged. The node image ships a `node` user for exactly this. RUN chown -R node:node /app USER node EXPOSE 8920 # The health endpoint is unauthenticated by design, so this works without # credentials baked into the image. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:8920/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" CMD ["pnpm", "exec", "tsx", "apps/api/src/server.ts"]