Optimizing Docker Builds for Node.js Production
A naive Dockerfile for a Node.js app can easily result in images over 1GB, slow deployments, and security vulnerabilities. Here is how I optimize Docker images to be under 100MB and secure.
1. Use Lightweight Base Images
Stop using FROM node:20. It contains a full Debian OS.
Use FROM node:20-alpine. It's only ~50MB.
2. Multi-Stage Builds
We don't need Typescript source files or devDependencies in production.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
# Install ALL deps for building
RUN npm ci
COPY . .
RUN npm run build# Stage 2: Runner FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV production COPY package*.json ./ # Install ONLY prod deps RUN npm ci --only=production # Copy built assets from builder COPY --from=builder /app/dist ./dist
CMD ["node", "dist/main.js"]
`
3. Layer Caching
Order matters! Docker caches layers.
# BAD
COPY . .
RUN npm install# GOOD
COPY package.json package-lock.json ./
RUN npm install
COPY . .
`
In the GOOD example, if you change a source file but not dependencies, Docker skips npm install and uses the cache. This speeds up builds by 10x.
4. Security Practices
- Don't run as root: By default, Docker runs as root. This is dangerous.
- Use Tini/Dumb-init: Node.js doesn't handle PID 1 signals (like SIGTERM/SIGINT) well. Use an init system to handle graceful shutdowns.
5. .dockerignore
Don't copy unnecessary files.
node_modules
dist
.git
.env
Dockerfile
README.md
By applying these patterns, we reduced our image size from 950MB to 78MB and build times from 4 mins to 45 seconds.