nodejs3 Min Read

Docker and Containerization for Node.js

Gorav Singal

April 02, 2026

TL;DR

Use multi-stage Docker builds to keep images small (<200MB). Run as non-root user, use .dockerignore, pin exact versions, add health checks, and use Docker Compose for local development with databases and Redis.

Docker and Containerization for Node.js

Why Docker for Node.js

Docker eliminates “works on my machine” problems by packaging your Node.js app with its exact runtime, dependencies, and configuration into a portable container.

Basic Dockerfile

FROM node:20-alpine

WORKDIR /app

# Copy package files first (layer caching)
COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

Multi-Stage Build (Production)

Multi-stage builds dramatically reduce image size by separating build dependencies from the runtime.

Multi-Stage Build

# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY tsconfig.json ./
COPY src/ ./src/

RUN npm run build
RUN npm prune --production

# Stage 2: Production
FROM node:20-alpine

# Security: run as non-root user
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

# Copy only what we need from build stage
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./

USER appuser

EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

.dockerignore

node_modules
npm-debug.log
.git
.env
.env.*
dist
coverage
.nyc_output
*.md
.vscode
.idea
docker-compose*.yml
Dockerfile*
.github
tests
__tests__

Docker Compose for Development

Docker Compose Stack

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - '3000:3000'
      - '9229:9229'  # Node.js debugger
    volumes:
      - .:/app
      - /app/node_modules  # Don't mount node_modules
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://postgres:postgres@db:5432/myapp_dev
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    command: npx nodemon --inspect=0.0.0.0:9229 src/server.ts

  db:
    image: postgres:16-alpine
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp_dev
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redisdata:/data

  mailhog:
    image: mailhog/mailhog
    ports:
      - '1025:1025'  # SMTP
      - '8025:8025'  # Web UI

volumes:
  pgdata:
  redisdata:

Development Dockerfile

# Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install  # Include devDependencies

COPY . .

EXPOSE 3000 9229

CMD ["npx", "nodemon", "src/server.ts"]

Health Checks

// health endpoint in your Express app
app.get('/health', async (req, res) => {
  const checks = {
    uptime: process.uptime(),
    timestamp: Date.now(),
    database: 'unknown',
    redis: 'unknown',
  };

  try {
    await db.query('SELECT 1');
    checks.database = 'healthy';
  } catch (err) {
    checks.database = 'unhealthy';
  }

  try {
    await redis.ping();
    checks.redis = 'healthy';
  } catch (err) {
    checks.redis = 'unhealthy';
  }

  const isHealthy = checks.database === 'healthy' && checks.redis === 'healthy';
  res.status(isHealthy ? 200 : 503).json(checks);
});

Security Best Practices

# 1. Use specific version tags (not :latest)
FROM node:20.11.1-alpine3.19

# 2. Run as non-root user
USER node

# 3. Use minimal base images (alpine = ~5MB vs debian = ~120MB)

# 4. Don't store secrets in the image
# Use environment variables or secret managers at runtime

# 5. Scan for vulnerabilities
# docker scout cves myimage:latest

Layer Caching Optimization

# Order matters! Least-changing layers first

# 1. Base image (rarely changes)
FROM node:20-alpine

WORKDIR /app

# 2. Dependencies (changes when package.json changes)
COPY package*.json ./
RUN npm ci --only=production

# 3. Application code (changes most frequently)
COPY . .

If you only change application code, Docker reuses cached layers for steps 1-2 — builds go from minutes to seconds.

Environment Variables and Secrets

# docker-compose.yml — development secrets
services:
  app:
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    env_file:
      - .env.development

# Production: use AWS Secrets Manager or Docker secrets
# NEVER bake secrets into the Docker image

Useful Commands

# Build and tag
docker build -t myapp:1.0.0 .

# Run with environment variables
docker run -p 3000:3000 --env-file .env myapp:1.0.0

# Compose commands
docker compose up -d          # Start all services
docker compose logs -f app    # Follow app logs
docker compose exec app sh    # Shell into container
docker compose down -v        # Stop and remove volumes

# Check image size
docker images myapp

# Multi-platform build (for ARM + x86)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

Production vs Development Config

Setting Development Production
Base image node:20-alpine node:20-alpine (multi-stage)
Dependencies All (dev + prod) Production only
Volumes Source code mounted No mounts
Debugger Port 9229 exposed Not exposed
Restart policy No unless-stopped
Logging Console (pretty) JSON to stdout
Image size ~400MB ~150MB

Docker transforms deployment from a manual, error-prone process into a repeatable, versioned, and testable pipeline.

Share

Related Posts

Deploying Node.js to AWS

Deploying Node.js to AWS

Choosing Your Deployment Strategy Service Best For Scaling Cold Start ECS…

WebSockets with Socket.io in Node.js

WebSockets with Socket.io in Node.js

WebSocket vs HTTP Traditional HTTP follows a request/response model — the client…

Testing Node.js — Unit, Integration, and E2E

Testing Node.js — Unit, Integration, and E2E

Testing Strategy A solid testing strategy follows the testing pyramid — many…

Redis — Caching, Sessions, Pub/Sub in Node.js

Redis — Caching, Sessions, Pub/Sub in Node.js

Why Redis for Node.js Redis is an in-memory data store that serves as a cache…

Database Integration — PostgreSQL with Node.js

Database Integration — PostgreSQL with Node.js

Choosing Your PostgreSQL Client Node.js has three main approaches to working…

Performance Optimization and Profiling in Node.js

Performance Optimization and Profiling in Node.js

Profiling First, Optimize Second Never optimize blindly. Always profile to find…

Latest Posts

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI video generation went from “cool demo” to “usable in production” in 2024-202…

AI Models in 2025 — Cost, Capabilities, and Which One to Use

AI Models in 2025 — Cost, Capabilities, and Which One to Use

Choosing the right AI model is one of the most impactful decisions you’ll make…

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

Generating one image with AI costs between $0.002 and $0.12. That might sound…

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

Two years ago, AI coding meant one thing: GitHub Copilot autocompleting your…

AI Agents Demystified — It's Just Automation With a Better Brain

AI Agents Demystified — It's Just Automation With a Better Brain

Let’s cut through the noise. If you read Twitter or LinkedIn, you’d think “AI…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…