```dockerfile # Stage 1: Builder FROM node:20-alpine AS builder WORKDIR /app
# Install pnpm globally RUN npm install -g pnpm
# Copy pnpm lockfile and package.json first for caching COPY package.json pnpm-lock.yaml ./
# Install production dependencies only, leveraging pnpm's content-addressable store RUN pnpm fetch --prod RUN pnpm install --prod --frozen-lockfile
# Copy application source code COPY . .
# If your application has a build step (e.g., TypeScript compilation, Webpack) # RUN pnpm build
# Stage 2: Runner FROM node:20-alpine AS runner WORKDIR /app
# Copy only the necessary production dependencies and built artifacts from the builder stage COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json # If there's a build output, copy it # COPY --from=builder /app/dist ./dist
# Use a non-root user for security best practices USER node
# Expose the port your application listens on EXPOSE 3000
# Command to start the application CMD ["pnpm", "start"] ```
``yaml # docker-compose.yml snippet for local development version: '3.8' services: app: build: context: . dockerfile: Dockerfile args: NODE_ENV: development # Override for development builds ports: - "3000:3000" volumes: - .:/app # Mount host directory for live code changes - /app/node_modules # Prevent host node_modules from overriding container's environment: NODE_ENV: development PORT: 3000 ``
Illustrative Build Argument:
To build for production, you would typically pass NODE_ENV: docker build --build-arg NODE_ENV=production -t my-node-app:1.0.0 .
Notes on Optimization and Security:
* Multi-stage Build: The primary technique, separating the build environment (compiler, dev dependencies) from the minimal runtime. Only essential artifacts (application code, production node_modules) are copied to the final runner image. * Alpine Base Image: node:20-alpine is used for both stages, significantly reducing the base image size compared to Debian-based images. * Pnpm `--prod` flag: pnpm install --prod ensures only production dependencies are installed and copied, omitting development and optional packages.
* Non-Root User: The USER node directive runs the application as an unprivileged user, mitigating potential container escape vulnerabilities should the application be compromised. * Minimal Base Image: Alpine's smaller footprint inherently reduces the attack surface by including fewer system utilities and libraries. * Frozen Lockfile: pnpm install --frozen-lockfile enforces reproducible dependency installations, preventing unexpected changes or supply chain attacks from modified pnpm-lock.yaml files. * Specific Node.js Version: Pinning to node:20-alpine provides stability and predictable security patching from official Docker images.