Node.js Docker — Complete Guide to Multi-Stage Builds and Production Containers
In this tutorial, you will learn about Node.js Docker. We cover key concepts, practical examples, and best practices to help you master this topic.
Node.js Docker containers package applications with their runtime environment, using multi-stage builds to create small production images with only necessary dependencies and artifacts.
What You'll Learn
By the end of this tutorial, you'll create multi-stage Dockerfiles, optimize image size with .dockerignore, configure health checks, manage environment variables, and follow security best practices.
Why Docker Matters
Docker ensures identical environments across development, testing, and production. It eliminates "it works on my machine" problems and simplifies deployment.
Real-World Use
A Node.js microservice is built from source in a Node image, then copied to a distroless runtime image. The final image is under 150MB and has zero OS vulnerabilities.
Docker Path
flowchart LR
A[PM2] --> B[Docker]
B --> C[Security]
C --> D[Deployment]
D --> E[CI/CD]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Multi-Stage Dockerfile
Multi-stage builds separate build and runtime environments for minimal final images.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup --system app && adduser --system --ingroup app app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Dockerignore
.dockerignore prevents unnecessary files from being sent to the Docker build context.
node_modules/
npm-debug.log*
.git/
.gitignore
.env
.env.*
.vscode/
.idea/
coverage/
test/
tests/
*.md
.dockerignore
Dockerfile
docker-compose*
Layer Caching Optimization
Optimize Dockerfile order to maximize layer caching. Copy package.json separately from source code.
# Optimized for layer caching
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files first (changes rarely)
COPY package*.json ./
RUN npm ci --only=production
# Copy source code (changes frequently)
COPY . .
RUN npm run build
# Layers 1-3 are cached unless package.json changes
Production Image Hardening
Use non-root users, read-only filesystems, and minimal base images for security.
FROM node:20-alpine AS runner
WORKDIR /app
# Create non-root user
RUN addgroup -S app && adduser -S -G app app
# Copy only necessary files
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
# Switch to non-root user
USER app
# Use read-only root filesystem
# docker run --read-only --tmpfs /tmp ...
EXPOSE 3000
CMD ["node", "--enable-source-maps", "dist/server.js"]
Docker Compose for Development
Use docker-compose for multi-service development environments.
version: "3.8"
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
- postgres
- redis
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
redis:
image: redis:7-alpine
Common Mistakes
1. Copying node_modules from Host
Always run npm ci inside the container. Host node_modules may have platform-specific binaries.
2. Running as Root
Root inside containers is the same as root on the host. Use non-root users.
3. Not Setting Health Checks
Without health checks, orchestrators cannot detect unresponsive containers.
4. Using Full Base Images
node:20-slim is 200MB. node:20-alpine is 120MB. Distroless images are under 100MB.
5. Ignoring Signal Handling
Node.js does not forward SIGTERM to child processes. Use tini or --init flag.
Practice Questions
1. What is the purpose of multi-stage builds?
Separate build and runtime environments. Build stage has dev tools, runtime stage has only what is needed.
2. Why should you copy package.json separately from source code?
To leverage Docker layer caching. Dependency installation is cached unless package.json changes.
3. What does the --init flag do in Docker for Node.js?
docker run --init ensures proper signal handling (SIGTERM forwarding) for Node.js processes.
4. How do you avoid running Node.js as root in Docker?
Create a non-root user with adduser/addgroup and use USER directive.
5. Challenge: Create a Dockerfile that produces a distroless multi-stage image.
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/server.js"]
FAQ
Mini Project: Docker Build and Deploy Script
Build a script that builds, tags, and pushes Docker images.
const { execSync } = require("node:child_process");
const pkg = require("./package.json");
const imageName = `myapp/api-server:${pkg.version}`;
console.log(`Building ${imageName}...`);
execSync(`docker build -t ${imageName} .`, { stdio: "inherit" });
execSync(`docker tag ${imageName} myapp/api-server:latest`, { stdio: "inherit" });
console.log(`Image ${imageName} built successfully`);
console.log(`Size:`, execSync(`docker images ${imageName} --format "{{.Size}}"`).toString().trim());
What's Next
Node.js Security Checklist Node.js Deployment Node.js PM2
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro