# syntax=docker/dockerfile:1 # ============================================================================= # Portal (Next.js) Dockerfile # ============================================================================= # Multi-stage build with standalone output for minimal image size # Optimized for fast builds, security, and small production images # ============================================================================= ARG NODE_VERSION=22 ARG PNPM_VERSION=10.25.0 # ============================================================================= # Stage 1: Dependencies (cached layer) # ============================================================================= FROM node:${NODE_VERSION}-alpine AS deps ARG PNPM_VERSION # Install build dependencies RUN apk add --no-cache libc6-compat \ && corepack enable \ && corepack prepare pnpm@${PNPM_VERSION} --activate WORKDIR /app # Copy manifests first for dependency caching (all workspace packages) COPY .npmrc pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/domain/package.json ./packages/domain/package.json COPY apps/portal/package.json ./apps/portal/package.json COPY apps/bff/package.json ./apps/bff/package.json # Install only the packages needed for the portal (domain + portal) ENV HUSKY=0 RUN --mount=type=cache,id=pnpm-portal,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile --filter @customer-portal/domain... --filter @customer-portal/portal... # ============================================================================= # Stage 2: Builder # ============================================================================= FROM deps AS builder # Copy source files COPY tsconfig.json tsconfig.base.json ./ COPY packages/domain/ ./packages/domain/ COPY apps/portal/ ./apps/portal/ # Build-time environment variables ARG NEXT_PUBLIC_API_BASE=/api ARG NEXT_PUBLIC_APP_NAME="Customer Portal" ARG NEXT_PUBLIC_APP_VERSION=1.0.0 ENV NODE_ENV=production \ NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE} \ NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} \ NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION} \ NEXT_TELEMETRY_DISABLED=1 # Build: domain → Next.js (standalone output) RUN pnpm --filter @customer-portal/domain build \ && pnpm --filter @customer-portal/portal build # ============================================================================= # Stage 3: Production # ============================================================================= FROM node:${NODE_VERSION}-alpine AS production LABEL org.opencontainers.image.title="Customer Portal Frontend" \ org.opencontainers.image.description="Next.js Customer Portal" \ org.opencontainers.image.vendor="Customer Portal" # Minimal runtime dependencies + security hardening RUN apk add --no-cache dumb-init libc6-compat \ && addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs \ # Remove apk cache and unnecessary files && rm -rf /var/cache/apk/* /tmp/* /root/.npm WORKDIR /app # Copy standalone build artifacts with correct ownership COPY --from=builder --chown=nextjs:nodejs /app/apps/portal/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/apps/portal/.next/static ./apps/portal/.next/static COPY --from=builder --chown=nextjs:nodejs /app/apps/portal/public ./apps/portal/public # Security: Run as non-root user USER nextjs # Expose frontend port EXPOSE 3000 # Environment configuration ENV NODE_ENV=production \ NEXT_TELEMETRY_DISABLED=1 \ PORT=3000 \ HOSTNAME="0.0.0.0" \ # Node.js production optimizations NODE_OPTIONS="--max-old-space-size=512" # Health check for container orchestration HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD node -e "fetch('http://localhost:3000/api/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))" ENTRYPOINT ["dumb-init", "--"] CMD ["node", "apps/portal/server.js"]