docker
Multi-Stage Dockerfile
Multi-stage Dockerfile pattern for minimal production images with separate build, test, and runtime stages.
Overview
Multi-stage builds use multiple FROM statements to separate build-time dependencies from the final runtime image. This results in dramatically smaller, more secure production images while maintaining a single Dockerfile for all environments.
Configuration
# Dockerfile
# ══════════════════════════════════════
# Stage 1: Dependencies
# ══════════════════════════════════════
FROM node:20-alpine AS deps
WORKDIR /app
# Copy dependency manifests
COPY package.json package-lock.json ./
# Install all dependencies (including dev for building)
RUN npm ci
# ══════════════════════════════════════
# Stage 2: Build
# ══════════════════════════════════════
FROM deps AS build
WORKDIR /app
# Copy source code
COPY tsconfig.json ./
COPY src/ ./src/
COPY public/ ./public/
# Build the application
RUN npm run build
# Prune devDependencies after build
RUN npm prune --omit=dev
# ══════════════════════════════════════
# Stage 3: Test (optional, for CI)
# ══════════════════════════════════════
FROM deps AS test
WORKDIR /app
# Copy everything needed for tests
COPY . .
# Run tests — this stage fails the build if tests fail
RUN npm run test -- --ci --coverage
# Run linting
RUN npm run lint
# ══════════════════════════════════════
# Stage 4: Production
# ══════════════════════════════════════
FROM node:20-alpine AS production
# Install tini for signal handling and dumb-init alternative
RUN apk add --no-cache tini
# Create non-root user
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# Copy only production dependencies (pruned in build stage)
COPY --from=build /app/node_modules ./node_modules
# Copy built output
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Switch to non-root user
USER app
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then(r => {if(!r.ok) process.exit(1)})"
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]
# ══════════════════════════════════════
# Stage 5: Development
# ══════════════════════════════════════
FROM deps AS development
WORKDIR /app
# Copy source (will be overridden by volume mount in docker-compose)
COPY . .
EXPOSE 3000 9229
CMD ["npx", "nodemon", "--inspect=0.0.0.0:9229", "src/index.ts"]
Key Options Explained
FROM ... AS name— Names each stage so other stages can reference it withCOPY --from=name. Only the final targeted stage ends up in the output image.docker build --target production .— Builds only up to the specified stage. Use--target developmentfor dev,--target testfor CI.- Stage separation — The
depsstage is shared betweenbuild,test, anddevelopment, so dependencies are only installed once. npm prune --omit=dev— After building, removes devDependencies so only production deps are copied to the final image.- Test stage — Runs during CI with
docker build --target test .. If tests fail, the build fails. Does not contribute to the production image. - Each stage starts fresh — Only explicitly
COPY --fromartifacts carry between stages. Build tools, source code, and dev dependencies are automatically excluded from production.
Common Modifications
- Go application: Replace Node stages with
FROM golang:1.22-alpine AS buildandFROM scratch AS productionfor an ultra-minimal binary image. - Frontend + Backend: Add a
frontend-buildstage withFROM node:20-alpine AS frontend-buildthat builds the React/Vue app, then copy the output to the backend stage. - CI integration: Use
docker build --target test .in CI pipelines. Extract coverage reports withdocker cp. - Build arguments: Add
ARG VITE_API_URLbefore build commands to inject environment-specific values at build time. - Cache mounts: Use
RUN --mount=type=cache,target=/root/.npm npm cito cache npm downloads across builds (BuildKit required).