Skip to content

Node.js Docker — Complete Guide to Multi-Stage Builds and Production Containers

DodaTech Updated 2026-06-28 4 min read

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

What is the difference between Alpine and Slim images?

Alpine uses musl libc (5MB base). Slim uses glibc (50MB base). Some native modules require glibc.

How do I handle environment variables in Docker?

Use --env-file for files or -e for individual vars. Never embed secrets in the image.

Should I use PM2 inside Docker?

Optional. PM2 cluster mode helps in multi-core containers. Combine with Docker restart policies.

How do I debug a Node.js container?

Run with --inspect flag and expose port 9229. Connect via Chrome DevTools.

What is the best base image for Node.js?

node:20-alpine for most cases. Distroless for security-sensitive deployments.

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