Containerization with Docker
Docker provides consistent, isolated environments for microservices. Learn to build optimized containers and orchestrate multiple services.Learning Objectives:
- Write production-ready Dockerfiles
- Implement multi-stage builds
- Optimize image size and build time
- Use docker-compose for local development
- Apply container security best practices
Docker Fundamentals for Microservices
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ WHY DOCKER FOR MICROSERVICES? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ WITHOUT CONTAINERS │ │
│ │ │ │
│ │ Developer Machine Staging Production │ │
│ │ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Node 16 │ │ Node 14 │ │ Node 18 │ │ │
│ │ │ npm 7 │ │ npm 6 │ │ npm 9 │ │ │
│ │ │ Linux Mint │ │ Ubuntu 20.04 │ │ Amazon Linux │ │ │
│ │ └──────────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ │ "It works on my machine!" 🤷 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ WITH CONTAINERS │ │
│ │ │ │
│ │ Developer Machine Staging Production │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐│ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐ ││ │
│ │ │ │ Container │ │ │ │ Container │ │ │ │ Container │ ││ │
│ │ │ │ Node 20 │ │ │ │ Node 20 │ │ │ │ Node 20 │ ││ │
│ │ │ │ Alpine │ │ │ │ Alpine │ │ │ │ Alpine │ ││ │
│ │ │ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘ ││ │
│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘│ │
│ │ │ │
│ │ Same container everywhere! ✅ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Production-Ready Dockerfile
Basic Node.js Dockerfile
Copy
# Dockerfile - Basic
FROM node:20-alpine
WORKDIR /app
# Copy package files first (better caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Start application
CMD ["node", "src/index.js"]
Multi-Stage Build (Optimized)
Copy
# Dockerfile - Multi-stage build
# ================================
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install ALL dependencies (including devDependencies for build)
RUN npm ci
# ================================
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
# Copy source code
COPY . .
# Build the application (TypeScript, bundling, etc.)
RUN npm run build
# Prune devDependencies
RUN npm prune --production
# ================================
# Stage 3: Production
FROM node:20-alpine AS production
WORKDIR /app
# Add labels for image metadata
LABEL org.opencontainers.image.source="https://github.com/myorg/order-service"
LABEL org.opencontainers.image.description="Order Service"
LABEL org.opencontainers.image.version="1.0.0"
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy built application
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 ./
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# Use dumb-init as entrypoint
ENTRYPOINT ["dumb-init", "--"]
# Start application
CMD ["node", "dist/index.js"]
TypeScript Specific Dockerfile
Copy
# TypeScript Service Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY tsconfig*.json ./
RUN npm ci
COPY src ./src
# Type checking and build
RUN npm run type-check && npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
RUN apk add --no-cache dumb-init && \
addgroup -S app && adduser -S app -G app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production && npm cache clean --force
USER app
ENV NODE_ENV=production
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
Docker Optimization
Layer Caching Strategy
Copy
# Optimize layer caching
# ================================
# Base image (rarely changes)
FROM node:20-alpine
WORKDIR /app
# 1. System dependencies (rarely change)
RUN apk add --no-cache dumb-init
# 2. Package files (change sometimes)
COPY package*.json ./
# 3. Dependencies (change when package.json changes)
RUN npm ci --only=production
# 4. Application code (changes frequently)
COPY . .
# Order matters: least changing layers first!
Reducing Image Size
Copy
# Size optimization techniques
# 1. Use slim base images
FROM node:20-alpine # ~50MB vs node:20 ~350MB
# 2. Clean up in same layer
RUN npm ci --only=production && \
npm cache clean --force && \
rm -rf /tmp/*
# 3. Use .dockerignore
# .dockerignore file:
# node_modules
# npm-debug.log
# Dockerfile*
# docker-compose*
# .git
# .gitignore
# .env*
# *.md
# tests
# coverage
# .nyc_output
# 4. Multi-stage builds (shown above)
# 5. Don't install dev dependencies in production
RUN npm ci --only=production
.dockerignore
Copy
# .dockerignore
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Git
.git
.gitignore
.gitattributes
# Docker
Dockerfile*
docker-compose*
.docker
# IDE
.idea
.vscode
*.swp
*.swo
# Test
tests
__tests__
coverage
.nyc_output
jest.config.*
# Docs
*.md
docs
# Environment files
.env*
!.env.example
# Build artifacts
dist
build
# Misc
.DS_Store
Thumbs.db
Docker Compose for Microservices
Complete Microservices Stack
Copy
# docker-compose.yml
version: '3.8'
services:
# ===================
# API Gateway
# ===================
api-gateway:
build:
context: ./services/api-gateway
dockerfile: Dockerfile
ports:
- "8080:3000"
environment:
- NODE_ENV=development
- ORDER_SERVICE_URL=http://order-service:3000
- PAYMENT_SERVICE_URL=http://payment-service:3000
- INVENTORY_SERVICE_URL=http://inventory-service:3000
- JWT_SECRET=${JWT_SECRET}
- REDIS_URL=redis://redis:6379
depends_on:
- order-service
- payment-service
- inventory-service
- redis
networks:
- microservices
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# ===================
# Order Service
# ===================
order-service:
build:
context: ./services/order-service
dockerfile: Dockerfile
target: development
volumes:
- ./services/order-service:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@order-db:5432/orders
- KAFKA_BROKERS=kafka:9092
- REDIS_URL=redis://redis:6379
depends_on:
order-db:
condition: service_healthy
kafka:
condition: service_healthy
networks:
- microservices
order-db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=orders
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- order-db-data:/var/lib/postgresql/data
- ./services/order-service/db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- microservices
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# ===================
# Payment Service
# ===================
payment-service:
build:
context: ./services/payment-service
dockerfile: Dockerfile
target: development
volumes:
- ./services/payment-service:/app
- /app/node_modules
environment:
- NODE_ENV=development
- MONGODB_URI=mongodb://payment-db:27017/payments
- STRIPE_API_KEY=${STRIPE_API_KEY}
- KAFKA_BROKERS=kafka:9092
depends_on:
payment-db:
condition: service_healthy
networks:
- microservices
payment-db:
image: mongo:6
volumes:
- payment-db-data:/data/db
networks:
- microservices
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 5s
retries: 5
# ===================
# Inventory Service
# ===================
inventory-service:
build:
context: ./services/inventory-service
dockerfile: Dockerfile
target: development
volumes:
- ./services/inventory-service:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@inventory-db:5432/inventory
- REDIS_URL=redis://redis:6379
depends_on:
inventory-db:
condition: service_healthy
networks:
- microservices
inventory-db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=inventory
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- inventory-db-data:/var/lib/postgresql/data
networks:
- microservices
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# ===================
# Infrastructure
# ===================
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- microservices
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
kafka:
image: confluentinc/cp-kafka:7.5.0
environment:
KAFKA_NODE_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk'
volumes:
- kafka-data:/var/lib/kafka/data
networks:
- microservices
healthcheck:
test: kafka-topics --bootstrap-server localhost:9092 --list
interval: 30s
timeout: 10s
retries: 5
# ===================
# Observability
# ===================
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./infrastructure/prometheus:/etc/prometheus
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
networks:
- microservices
grafana:
image: grafana/grafana:latest
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
- ./infrastructure/grafana/provisioning:/etc/grafana/provisioning
networks:
- microservices
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
- "4317:4317"
environment:
- COLLECTOR_OTLP_ENABLED=true
networks:
- microservices
networks:
microservices:
driver: bridge
volumes:
order-db-data:
payment-db-data:
inventory-db-data:
redis-data:
kafka-data:
prometheus-data:
grafana-data:
Development Dockerfile with Hot Reload
Copy
# Dockerfile with development and production targets
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache dumb-init
# Development stage
FROM base AS development
# Install all dependencies including devDependencies
COPY package*.json ./
RUN npm install
# Mount source code as volume in docker-compose
# No COPY needed - will use volume mount
ENV NODE_ENV=development
EXPOSE 3000
# Use nodemon for hot reload
CMD ["npx", "nodemon", "--watch", "src", "src/index.js"]
# Production stage
FROM base AS production
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN addgroup -S app && adduser -S app -G app
USER app
ENV NODE_ENV=production
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "src/index.js"]
Docker Compose Override for Development
Copy
# docker-compose.override.yml
version: '3.8'
# This file is automatically merged with docker-compose.yml
# for local development
services:
order-service:
build:
target: development
volumes:
- ./services/order-service/src:/app/src
command: npx nodemon --watch src src/index.js
ports:
- "3001:3000"
- "9229:9229" # Node debugger
payment-service:
build:
target: development
volumes:
- ./services/payment-service/src:/app/src
command: npx nodemon --watch src src/index.js
ports:
- "3002:3000"
- "9230:9229"
inventory-service:
build:
target: development
volumes:
- ./services/inventory-service/src:/app/src
command: npx nodemon --watch src src/index.js
ports:
- "3003:3000"
- "9231:9229"
Container Security
Security Best Practices
Copy
# Security-hardened Dockerfile
FROM node:20-alpine AS builder
# Update and patch base image
RUN apk update && apk upgrade
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Production stage with security hardening
FROM node:20-alpine
# Security updates
RUN apk update && \
apk upgrade && \
apk add --no-cache dumb-init && \
rm -rf /var/cache/apk/*
WORKDIR /app
# Create non-root user with specific UID/GID
RUN addgroup -S -g 1001 appgroup && \
adduser -S -u 1001 -G appgroup appuser
# Copy with correct ownership
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app .
# Remove unnecessary files
RUN rm -rf .git .gitignore .env* Dockerfile* docker-compose* && \
chmod -R 500 /app && \
chmod -R 400 /app/node_modules
# Set security environment variables
ENV NODE_ENV=production
ENV NPM_CONFIG_LOGLEVEL=warn
# Read-only root filesystem support
# (use with --read-only in docker run)
RUN mkdir -p /tmp && chown appuser:appgroup /tmp
# Switch to non-root user
USER appuser
# No capabilities needed
# Use with: docker run --cap-drop=ALL
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node healthcheck.js
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
Healthcheck Script
Copy
// healthcheck.js
const http = require('http');
const options = {
hostname: 'localhost',
port: process.env.PORT || 3000,
path: '/health',
method: 'GET',
timeout: 5000
};
const req = http.request(options, (res) => {
process.exit(res.statusCode === 200 ? 0 : 1);
});
req.on('error', () => {
process.exit(1);
});
req.on('timeout', () => {
req.destroy();
process.exit(1);
});
req.end();
Security Scanning
Copy
# .github/workflows/docker-security.yml
name: Docker Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Run Hadolint
uses: hadolint/[email protected]
with:
dockerfile: Dockerfile
Docker Commands Reference
Copy
# Build commands
docker build -t order-service:1.0.0 .
docker build -t order-service:1.0.0 --target production .
docker build --no-cache -t order-service:1.0.0 .
# Run commands
docker run -d --name order -p 3000:3000 order-service:1.0.0
docker run -d --read-only --cap-drop=ALL -p 3000:3000 order-service:1.0.0
docker run --rm -it order-service:1.0.0 /bin/sh
# Compose commands
docker-compose up -d
docker-compose up --build
docker-compose down
docker-compose down -v # Remove volumes
docker-compose logs -f order-service
docker-compose exec order-service /bin/sh
# Debug commands
docker logs -f order-service
docker exec -it order-service /bin/sh
docker inspect order-service
docker stats
# Cleanup
docker system prune -a
docker volume prune
docker image prune -a
Interview Questions
Q1: What is multi-stage build and why use it?
Q1: What is multi-stage build and why use it?
Answer:Multi-stage build uses multiple Typical reduction: 500MB → 100MB
FROM statements to create intermediate images.Benefits:- Smaller final images (only runtime dependencies)
- Separate build and runtime environments
- Don’t expose build tools in production
- Better security (fewer attack vectors)
Copy
FROM node:20 AS builder
RUN npm ci && npm run build
FROM node:20-alpine AS production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
Q2: How do you optimize Docker layer caching?
Q2: How do you optimize Docker layer caching?
Answer:Key principles:Tips:
- Order instructions by change frequency (least → most)
- Copy dependency files before source code
- Combine RUN commands to reduce layers
Copy
# Good: package.json rarely changes
COPY package*.json ./
RUN npm ci
# Then source code (changes often)
COPY . .
- Use
.dockerignoreto exclude unnecessary files - Pin versions to ensure consistent builds
- Use
--no-cacheonly when needed
Q3: What are Docker security best practices?
Q3: What are Docker security best practices?
Answer:
-
Don’t run as root
Copy
RUN adduser -S app USER app -
Use minimal base images
- Alpine, distroless, slim variants
-
Scan for vulnerabilities
- Trivy, Snyk, Clair
-
Drop capabilities
Copy
docker run --cap-drop=ALL -
Read-only filesystem
Copy
docker run --read-only -
No secrets in images
- Use environment variables or secrets managers
-
Keep images updated
- Regularly rebuild with patched base images
Summary
Key Takeaways
- Multi-stage builds for optimized images
- Layer caching for faster builds
- Run as non-root user
- docker-compose for local development
- Scan images for vulnerabilities
Next Steps
In the next chapter, we’ll deploy to Kubernetes - the industry standard for container orchestration.