# ── Stage 1: Install dependencies ────────────────────────────── FROM node:24-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ COPY packages/web/package.json packages/web/ # Create minimal engine package.json for workspace resolution RUN mkdir -p packages/engine && echo '{"name":"@polyant/engine","version":"1.3.5","private":true}' > packages/engine/package.json RUN npm ci --workspace=@polyant/web --include-workspace-root # npm only creates this when the lockfile nests a dependency here; the build stage COPYs it RUN mkdir -p packages/web/node_modules # ── Stage 2: Build ───────────────────────────────────────────── FROM node:24-alpine AS build WORKDIR /app COPY --from=deps /app/node_modules ./node_modules # npm hoists most deps to the root, but workspace-local deps (e.g. lucide-react) # land in packages/web/node_modules — copy them too or Turbopack can't resolve them COPY --from=deps /app/packages/web/node_modules ./packages/web/node_modules COPY package.json package-lock.json ./ COPY packages/web/ packages/web/ # The web prebuild (scripts/generate-changelog.ts) reads the MONOREPO ROOT # CHANGELOG.md and hard-fails if it is missing — it must be in the build stage. COPY CHANGELOG.md ./ # Engine URL for the Next.js /api rewrites, inlined at build time. # Default "" = relative paths for gateway-routed topologies (AWS ALB routes # /api/* to the engine). Deployments where Next itself must proxy to the # engine (e.g. Render, separate web/engine services) MUST pass the engine # URL as a build arg — Render injects service env vars as build args when # they are declared via ARG. ARG NEXT_PUBLIC_API_URL="" ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} RUN npm run build -w @polyant/web # ── Stage 3: Runtime ─────────────────────────────────────────── FROM node:24-alpine AS runtime WORKDIR /app ENV NODE_ENV=production # Copy Next.js standalone output. The standalone build does NOT bundle the # public/ folder nor .next/static — both must be copied next to server.js # (which lives at packages/web/server.js in a monorepo standalone tree). COPY --from=build /app/packages/web/.next/standalone ./ COPY --from=build /app/packages/web/.next/static ./packages/web/.next/static COPY --from=build /app/packages/web/public ./packages/web/public EXPOSE 3000 # Next.js standalone server.js reads PORT/HOSTNAME at runtime. Without HOSTNAME # Docker auto-injects the container id, and the server binds to it instead of # all interfaces → unreachable on the published port. Set it here so the image # is self-contained (the AWS task def also sets it, but plain `docker run` / # compose / Render would not). ENV PORT=3000 ENV HOSTNAME="0.0.0.0" # Run as the image's unprivileged `node` user. The standalone server writes only # its image cache under .next/cache, created here and owned by it; the rest of # the tree stays root-owned and read-only to the process. RUN mkdir -p /app/packages/web/.next/cache && chown node:node /app/packages/web/.next/cache USER node # The sign-in page renders without a session or the engine, so it shows the # server itself is up. HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD wget -q -O /dev/null "http://127.0.0.1:${PORT}/login" || exit 1 CMD ["node", "packages/web/server.js"]