The "It works on my machine" problem has plagued software engineering for decades. A developer builds an app on a Mac using Node 18, hands it to a Linux production server running Node 14, and the app crashes due to missing system bindings. Docker solves this by packaging your application, its dependencies, the correct Node.js version, and the underlying OS libraries into a single, isolated, immutable "Container".


Module 1: Images vs. Containers

Before writing code, understand the terminology:

Docker Terminology

  • Dockerfile: The recipe. A text file containing the instructions to build an image.
  • Image: The compiled, immutable blueprint (e.g., a 200MB file containing Ubuntu, Node.js, and your app code).
  • Container: A running instance of an Image. You can run hundreds of identical containers from a single image.

Module 2: Layer Caching (The Secret to Fast Builds)

Docker builds images in layers. Each command in a Dockerfile creates a new layer. If a layer doesn't change, Docker skips it. You must order your Dockerfile to maximize this cache.

Dockerfile (Bad vs Good)dockerfile
# ❌ BAD DOCKERFILE
FROM node:18-alpine
WORKDIR /app
# Copying all source code invalidates the cache on EVERY code change
COPY . .
# This means npm install runs every single time you change a CSS file. Extremely slow.
RUN npm install
CMD ["npm", "start"]

# ✅ GOOD DOCKERFILE
FROM node:18-alpine
WORKDIR /app
# Only copy the package files first
COPY package*.json ./
# Run npm install. This layer is now cached UNLESS package.json changes.
RUN npm ci --only=production
# Now copy the frequently changing source code
COPY . .
CMD ["node", "server.js"]

Module 3: Multi-Stage Builds for Next.js

Modern JavaScript frameworks require massive dependencies to compile the code (Webpack, TypeScript, Babel). However, the production server doesn't need TypeScript to serve the generated HTML/JS. A multi-stage build creates a temporary container to build the app, then copies only the final artifacts to a tiny production container.

Dockerfile (Multi-Stage)dockerfile
# --- STAGE 1: Builder ---
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# This requires output: 'standalone' in next.config.js
RUN npm run build 

# --- STAGE 2: Production Runner ---
# We start fresh from a new, empty Alpine image
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

# Security: Never run containers as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs

# Copy only the compiled assets from the builder stage
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

EXPOSE 3000
ENV PORT 3000

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

Module 4: Orchestrating with Docker Compose

Your web app likely requires a database (Postgres) and a cache (Redis). Running these manually is tedious. docker-compose allows you to define your entire stack in one YAML file.

docker-compose.ymlyaml
version: '3.8'

services:
  # Your Node.js App
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      # Docker automatically resolves the hostname 'db' to the postgres container's IP
      - DATABASE_URL=postgres://admin:secret123@db:5432/myapp
    depends_on:
      - db

  # The PostgreSQL Database
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=secret123
      - POSTGRES_DB=myapp
    ports:
      - "5432:5432"
    volumes:
      # Mounts the data to your hard drive so it survives container restarts
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

To boot up the entire stack, a new developer on your team simply runs: docker-compose up -d. They don't need to install Postgres on their machine at all.