# syntax=docker/dockerfile:1 # ============================================================================= # BFF (NestJS) Dockerfile # ============================================================================= # Multi-stage build with BuildKit cache mounts for fast rebuilds # Optimized for minimal image size, security, and fast startup # ============================================================================= 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 in single layer RUN apk add --no-cache python3 make g++ openssl 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/bff/package.json ./apps/bff/package.json COPY apps/portal/package.json ./apps/portal/package.json # Install only the packages needed for the BFF (domain + bff) ENV HUSKY=0 RUN --mount=type=cache,id=pnpm-bff,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile --filter @customer-portal/domain... --filter @customer-portal/bff... # ============================================================================= # Stage 2: Builder # ============================================================================= FROM deps AS builder # Copy source files COPY tsconfig.json tsconfig.base.json tsconfig.node.json ./ COPY packages/domain/ ./packages/domain/ COPY apps/bff/ ./apps/bff/ # Build: domain → Prisma generate → BFF (single RUN for better layer efficiency) # Clean tsbuildinfo to force fresh emit; checked-in files can skip output otherwise RUN rm -f apps/bff/tsconfig*.tsbuildinfo \ && pnpm --filter @customer-portal/domain build \ && pnpm --filter @customer-portal/bff exec prisma generate \ && pnpm --filter @customer-portal/bff build # Create production deployment bundle with pnpm deploy RUN pnpm deploy --filter @customer-portal/bff --prod /app/deploy \ && cp -r apps/bff/dist deploy/dist \ && cp -r apps/bff/prisma deploy/prisma \ && cp -r packages/domain/dist deploy/node_modules/@customer-portal/domain/dist # Regenerate Prisma client in the flattened deploy layout so embedded schema path matches production WORKDIR /app/deploy RUN pnpm exec prisma generate --schema=prisma/schema.prisma WORKDIR /app # ============================================================================= # Stage 3: Production # ============================================================================= FROM node:${NODE_VERSION}-alpine AS production ARG PNPM_VERSION LABEL org.opencontainers.image.title="Customer Portal BFF" \ org.opencontainers.image.description="NestJS Backend-for-Frontend API" \ org.opencontainers.image.vendor="Customer Portal" # Install runtime dependencies only + security hardening RUN apk add --no-cache dumb-init libc6-compat netcat-openbsd \ && corepack enable \ && corepack prepare pnpm@${PNPM_VERSION} --activate \ && addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nestjs \ # Remove apk cache and unnecessary files && rm -rf /var/cache/apk/* /tmp/* /root/.npm WORKDIR /app # Set Prisma schema path before copying files ENV PRISMA_SCHEMA_PATH=/app/prisma/schema.prisma # Copy deploy bundle with correct ownership in single layer COPY --from=builder --chown=nestjs:nodejs /app/deploy ./ # Copy entrypoint and setup directories COPY --chown=nestjs:nodejs apps/bff/scripts/docker-entrypoint.sh ./docker-entrypoint.sh RUN chmod +x docker-entrypoint.sh \ && mkdir -p secrets logs \ && chown nestjs:nodejs secrets logs # Security: Run as non-root user USER nestjs # Expose BFF port EXPOSE 4000 # Environment configuration ENV NODE_ENV=production \ PORT=4000 \ # Node.js production optimizations NODE_OPTIONS="--max-old-space-size=512" # Health check for container orchestration HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD node -e "fetch('http://localhost:4000/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))" ENTRYPOINT ["dumb-init", "--", "./docker-entrypoint.sh"] CMD ["node", "dist/main.js"]