Difficulty: Intermediate
Docker packages your Node.js application and all its dependencies into a container - a lightweight, isolated environment that runs consistently everywhere. Whether you are running on your laptop, a CI server, or a production cloud instance, the container behaves identically. This eliminates the 'it works on my machine' problem and makes deployments reproducible and predictable.
A Dockerfile is a text file with instructions for building a Docker image. Each instruction creates a layer in the image. For Node.js apps, the typical flow is: start from a Node.js base image, copy package.json and install dependencies, copy your application code, and define the startup command. The order of instructions matters for layer caching - put things that change rarely (like npm install) before things that change often (like your source code) so Docker can reuse cached layers and rebuild faster.
Multi-stage builds are a powerful technique that significantly reduces image size. Instead of one stage that includes all build tools, dev dependencies, and source code, you use two stages: a 'builder' stage that installs all dependencies and compiles TypeScript, and a 'production' stage that copies only the compiled output and production dependencies. A typical Node.js image drops from 1GB+ to under 200MB with multi-stage builds, which means faster deploys and less storage.
Docker Compose orchestrates multiple containers that work together. A typical web application needs at least two containers: your Node.js app and a database (PostgreSQL, Redis, MongoDB). docker-compose.yml defines these services, their networks, and their volumes in one file. Running `docker-compose up` starts everything together, with proper networking so containers can communicate using service names as hostnames.
The .dockerignore file prevents unnecessary files from being copied into the image. Without it, Docker copies everything in your project directory - including node_modules, .git, .env files, and test files. A good .dockerignore significantly speeds up builds and reduces image size. It should include node_modules (they are installed fresh in the container), .git, .env, local IDE configs, and any other files not needed at runtime.
// Dockerfile (save as 'Dockerfile' in project root)
const dockerfile = `
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files first (layer caching)
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Copy only production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy built files from builder
COPY --from=builder /app/dist ./dist
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \\
adduser -S nodeuser -u 1001
USER nodeuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
`;
console.log('Multi-stage Dockerfile:');
console.log('Stage 1 (builder): install all deps, compile TypeScript');
console.log('Stage 2 (production): only production deps + compiled code');
console.log('Runs as non-root user for security');
console.log('Uses Alpine for minimal image size');
The builder stage installs all dependencies (including devDependencies like TypeScript) and compiles the code. The production stage starts fresh with only production dependencies and copies the compiled output. This removes TypeScript, test tools, and build artifacts from the final image.
// docker-compose.yml
const dockerCompose = `
version: '3.8'
services:
api:
build: ./server
ports:
- '3000:3000'
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
- JWT_SECRET=dev-jwt-secret-for-docker
depends_on:
db:
condition: service_healthy
volumes:
- ./server/src:/app/src # Hot reload in development
command: npx tsx watch src/index.ts
db:
image: postgres:16-alpine
ports:
- '5432:5432'
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: pg_isready -U postgres
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
pgdata:
`;
console.log('Docker Compose services:');
console.log('1. api - Node.js Express application');
console.log('2. db - PostgreSQL with health check');
console.log('3. redis - Redis for caching/sessions');
console.log('\nCommands:');
console.log(' docker-compose up -d # Start all services');
console.log(' docker-compose logs -f # Follow logs');
console.log(' docker-compose down # Stop and remove');
Docker Compose defines three services. The API service builds from the server Dockerfile, mounts source code for hot reload, and waits for the database to be healthy. The database uses a named volume for data persistence. Services communicate using service names as hostnames (e.g., 'db' instead of 'localhost').
// .dockerignore file
const dockerignore = `
node_modules
npm-debug.log*
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose.yml
.dockerignore
README.md
.vscode
.idea
coverage
tests
*.test.js
*.spec.js
`;
console.log('.dockerignore entries:');
const entries = dockerignore.trim().split('\n').filter(l => l.trim());
console.log(entries.length, 'patterns');
console.log('');
// Build commands
console.log('Docker build commands:');
console.log(' docker build -t myapp:latest .');
console.log(' docker build -t myapp:v1.0.0 --target production .');
console.log(' docker run -p 3000:3000 --env-file .env myapp:latest');
console.log('');
// Image size comparison
console.log('Image size comparison:');
console.log(' node:20 ~1.1GB');
console.log(' node:20-slim ~250MB');
console.log(' node:20-alpine ~180MB');
console.log(' Multi-stage app ~150MB');
The .dockerignore prevents unnecessary files from being sent to the Docker daemon during build. Using Alpine-based images and multi-stage builds dramatically reduces image size. The --target flag lets you build a specific stage (useful for building development vs production images from the same Dockerfile).
Dockerfile, multi-stage builds, docker-compose, .dockerignore, container, image, layer caching