Multi-stage Docker builds are the cornerstone of production-ready containerized deployments. By isolating the build environment from the runtime environment, we can reduce our image sizes by up to 90% and lock down security.
๐ฏ The Challenge
A naive Dockerfile copy-pastes the entire source directory, runs npm install, and fires up the app. This leaves unnecessary devDependencies, source maps, and system-level compilers inside the final production container. The result? 1.2GB images that take ages to pull and present a massive attack surface.
๐ก The Solution: Multi-Stage Orchestration
By defining distinct stages for dependencies installation, project building, and runtime execution, we only ship the absolute essentials.
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]๐ ๏ธ Key Takeaways
- โธLeast Privilege: Running containers as a non-root user (
nextjs) prevents potential directory escapes. - โธLayer Cache Optimization: Order operations from least-frequently changed to most-frequently changed. Copying
package.jsonand runningnpm cibefore copying source code ensures that dependency installs are cached. - โธAlpine Base Images: Minimalistic Linux distros reduce image size and shrink vulnerability counts down to near zero.