# Multi-stage Dockerfile for Next.js CMS # Stage 1: Build FROM node:22-alpine AS builder WORKDIR /app # Copy package files COPY package.json pnpm-lock.yaml ./ # Install dependencies (pnpm pinned so builds are reproducible) RUN npm install -g pnpm@10.22.0 && pnpm install --frozen-lockfile # Copy source code COPY . . # Refuse to build when a production dependency has a known critical # vulnerability. Runs after COPY so it isn't skipped by the layer cache on # deploys that only change source code. RUN pnpm audit --prod --audit-level critical ARG DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" ENV DATABASE_URL=$DATABASE_URL # Generate Prisma client RUN pnpm exec prisma generate # Build Next.js application RUN pnpm run build # Stage 2: Production Runtime FROM node:22-alpine WORKDIR /app # Install dumb-init for proper signal handling RUN apk add --no-cache dumb-init # Copy the standalone server (only the files it actually uses, including the # traced parts of node_modules), plus static assets, which it does not include. # Files stay root-owned so the app user cannot rewrite its own code; only the # Next.js cache directory is writable. COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public RUN mkdir -p .next/cache && chown node:node .next/cache # Set production environment. HOSTNAME makes the server listen on all # interfaces rather than only the container's hostname. ENV NODE_ENV=production ENV HOSTNAME=0.0.0.0 ENV PORT=3000 # Run as the unprivileged "node" user (uid 1000) instead of root USER node # Expose port 3000 EXPOSE 3000 # Use dumb-init to handle signals properly ENTRYPOINT ["dumb-init", "--"] # Start the standalone Next.js server CMD ["node", "server.js"]