Introduction
Docker packages your application and its dependencies into a container — a lightweight, portable unit that runs identically on any machine. This eliminates “works on my machine” problems and makes deployments reproducible. See Javascript Guide for more context. See Javascript Guide for more context.
Prerequisites: Docker installed (docs.docker.com/get-docker), basic command line knowledge.
Core Concepts
Image = a read-only template (like a class)
Container = a running instance of an image (like an object)
Registry = a storage service for images (Docker Hub, ECR, GCR)
Dockerfile = instructions to build an image
Your First Dockerfile
# Start from an official base image
FROM node:20-alpine
# Set working directory inside the container
WORKDIR /app
# Copy dependency files first (for layer caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Document which port the app uses
EXPOSE 3000
# Command to run when container starts
CMD ["node", "src/index.js"]
Build and run:
# Build the image
docker build -t myapp:1.0 .
# Run a container from the image
docker run -p 3000:3000 myapp:1.0
# Run in background (detached)
docker run -d -p 3000:3000 --name myapp myapp:1.0
# Check running containers
docker ps
# View logs
docker logs myapp
docker logs -f myapp # follow
# Stop and remove
docker stop myapp
docker rm myapp
Multi-Stage Builds
Multi-stage builds produce smaller production images by separating build tools from runtime:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # install ALL deps (including devDeps)
COPY . .
RUN npm run build # compile TypeScript, bundle, etc.
# Stage 2: Production image
FROM node:20-alpine AS production
WORKDIR /app
# Only copy what's needed to run
COPY package*.json ./
RUN npm ci --only=production # production deps only
COPY --from=builder /app/dist ./dist
# Run as non-root user (security)
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"
CMD ["node", "dist/index.js"]
Size comparison:
Without multi-stage: ~1.2GB (includes build tools, devDependencies)
With multi-stage: ~180MB (only runtime + production deps)
.dockerignore
Always create .dockerignore to exclude unnecessary files:
## .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
dist
coverage
*.test.ts
*.spec.ts
README.md
docker-compose*.yml
.dockerignore
Dockerfile*
Without this, COPY . . copies node_modules (hundreds of MB) into the build context.
Essential Docker Commands
# Images
docker images # list local images
docker pull node:20-alpine # download image
docker rmi myapp:1.0 # remove image
docker image prune # remove unused images
# Containers
docker ps # running containers
docker ps -a # all containers (including stopped)
docker run -it node:20-alpine sh # interactive shell
docker exec -it myapp sh # shell in running container
docker inspect myapp # detailed container info
docker stats # live resource usage
# Build
docker build -t myapp:1.0 .
docker build -t myapp:1.0 -f Dockerfile.prod . # custom Dockerfile
docker build --no-cache -t myapp:1.0 . # ignore cache
# Registry
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker push registry.example.com/myapp:1.0
docker pull registry.example.com/myapp:1.0
Environment Variables
# Set defaults in Dockerfile
ENV NODE_ENV=production
ENV PORT=3000
# Override at runtime
docker run -e NODE_ENV=staging -e PORT=8080 myapp:1.0
# Load from file
docker run --env-file .env.production myapp:1.0
Volumes: Persisting Data
# Named volume (managed by Docker)
docker run -v myapp-data:/app/data myapp:1.0
# Bind mount (maps host directory)
docker run -v $(pwd)/data:/app/data myapp:1.0
# Read-only bind mount
docker run -v $(pwd)/config:/app/config:ro myapp:1.0
Networking
# Create a network
docker network create myapp-network
# Run containers on the same network (they can reach each other by name)
docker run -d --name db --network myapp-network postgres:16
docker run -d --name app --network myapp-network -e DB_HOST=db myapp:1.0
# List networks
docker network ls
docker network inspect myapp-network
Dockerfile Best Practices
Layer Caching
Docker caches each layer. Put frequently-changing files last:
# GOOD: dependencies cached separately from code
COPY package*.json ./
RUN npm ci
COPY . . # code changes don't invalidate npm ci cache
# BAD: any code change invalidates npm ci
COPY . .
RUN npm ci
Minimize Layers
# BAD: 3 separate RUN layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# GOOD: single layer, smaller image
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
Use Specific Tags
# BAD: latest can change unexpectedly
FROM node:latest
# GOOD: pin to specific version
FROM node:20.11.0-alpine3.19
Non-Root User
# Create and use a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Debugging Containers
# Get a shell in a running container
docker exec -it myapp sh
# Get a shell in a stopped container
docker run -it --entrypoint sh myapp:1.0
# Copy files out of a container
docker cp myapp:/app/logs/error.log ./error.log
# View filesystem changes
docker diff myapp
# Check resource usage
docker stats myapp
# Inspect image layers
docker history myapp:1.0
Complete Node.js Example
# Dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# Development
docker build --target development -t myapp:dev .
docker run -v $(pwd)/src:/app/src -p 3000:3000 myapp:dev
# Production
docker build --target production -t myapp:prod .
docker run -p 3000:3000 myapp:prod
Comments