Dockerfile.web
Rama main · réplica de lectura del repositorio original.
# ── 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"]