# ─── Base ───────────────────────────────────────────────────────────────────── FROM node:20-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable && corepack prepare pnpm@latest-10 --activate # ─── Builder ────────────────────────────────────────────────────────────────── FROM base AS builder WORKDIR /app # Copy the full monorepo source (node_modules are excluded via .dockerignore) COPY . . # Install all workspace dependencies RUN pnpm install --frozen-lockfile # Build the React frontend # BASE_PATH=/ because in Docker there is no path prefix RUN BASE_PATH=/ PORT=7860 NODE_ENV=production \ pnpm --filter @workspace/app run build # Build the Express API server RUN pnpm --filter @workspace/api-server run build # Use pnpm deploy to produce a self-contained directory with real (non-symlinked) # node_modules — this is the canonical pnpm monorepo deployment pattern. # It resolves all workspace symlinks and prunes dev dependencies. RUN pnpm --filter @workspace/api-server deploy --prod --legacy /app/api-deploy # ─── Runner ─────────────────────────────────────────────────────────────────── FROM node:20-slim AS runner WORKDIR /app # API server compiled bundle (from the build step, not pnpm deploy's copy) COPY --from=builder /app/artifacts/api-server/dist ./dist # Self-contained node_modules from pnpm deploy (real files, no symlinks) COPY --from=builder /app/api-deploy/node_modules ./node_modules # package.json is read at startup by bare-server-node COPY --from=builder /app/api-deploy/package.json ./package.json # Vite-built frontend + UV/service-worker public files # Vite copies artifacts/app/public/** into the output during build COPY --from=builder /app/artifacts/app/dist/public ./public EXPOSE 7860 ENV PORT=7860 ENV NODE_ENV=production # PASSWORD must be set as a Space secret — the app will refuse auth without it # SESSION_SECRET is optional but recommended for production hardening CMD ["node", "--enable-source-maps", "./dist/index.mjs"]