Docker transforms the classic "works on my machine" problem into a solved one. A container packages your Node.js application with its exact runtime, dependencies, and configuration — ensuring identical behavior from a developer's laptop to a production cluster. Here is how I containerize Node.js applications for production.
The Naive Dockerfile (and Why It Is Bad)
Most tutorials start here:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "dist/index.js"]Problems:
- Copies
node_modules,.git, and other junk into the image. - Installs devDependencies in production.
- Uses the full Node.js image (1 GB+).
- No layer caching — every code change reinstalls all dependencies.
Multi-Stage Build (Production-Ready)
A multi-stage build separates the build environment from the production runtime:
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build TypeScript
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY src ./src
RUN npm run build
# Stage 3: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
# Create non-root user
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
USER appuser
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]Results:
- Final image size: ~150 MB (vs. 1 GB+ with the naive approach).
- No devDependencies, no source code, no build tools in production.
- Runs as a non-root user for security.
The .dockerignore File
Just as critical as the Dockerfile. Prevent unnecessary files from entering the build context:
node_modules
.git
.env
.env.*
dist
*.md
.vscode
coverage
testsA smaller build context means faster docker build commands.
Environment Variables
Never bake secrets into the image. Pass them at runtime:
docker run -d \
-e DATABASE_URL="mongodb://..." \
-e REDIS_URL="redis://..." \
-e JWT_SECRET="super-secret" \
-p 3000:3000 \
my-app:latestFor complex setups, use a .env file with Docker Compose (never commit it to Git).
Docker Compose for Development
Docker Compose orchestrates multi-container setups. A typical backend stack:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- DATABASE_URL=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
volumes:
- ./src:/app/src # hot reload in dev
mongo:
image: mongo:7
ports:
- '27017:27017'
volumes:
- mongo_data:/data/db
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
mongo_data:Start everything with one command:
docker compose up -dHealth Checks in Docker
Add a health check so Docker and orchestrators know when your app is ready:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/api/health || exit 1Layer Caching Strategy
Docker caches layers from top to bottom. Structure your Dockerfile so frequently changing layers are at the bottom:
1. Base image (rarely changes)
2. COPY package.json and npm ci (changes when dependencies change)
3. COPY src and npm run build (changes on every code push)
This way, a code change does not trigger a full npm ci — only the build step runs.
Production Deployment Checklist
- [ ] Use multi-stage builds to minimize image size.
- [ ] Run as a non-root user.
- [ ] Never bake secrets into images.
- [ ] Add a health check endpoint and Docker health check.
- [ ] Set
NODE_ENV=productionexplicitly. - [ ] Use
npm ciinstead ofnpm installfor deterministic builds. - [ ] Tag images with Git SHA, not
latest, for traceability. - [ ] Scan images for vulnerabilities with
docker scoutor Trivy.
Key Takeaways
- Multi-stage builds are non-negotiable for production Node.js images.
- Docker Compose gives you a one-command development environment.
- Structure Dockerfiles for maximum layer cache efficiency.
- Always run containers as non-root and scan for vulnerabilities.
Docker is the foundation of modern deployment workflows. Master it, and you unlock CI/CD pipelines, Kubernetes orchestration, and cloud-native architecture.
