Docker Containerization Best Practices
Comprehensive guide for building efficient, secure, and production-ready Docker containers
# Docker Containerization Best Practices
## 1. Dockerfile Optimization
### Multi-Stage Builds for Smaller Images
```dockerfile
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy source and build
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
# Switch to non-root user
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
```
### Layer Caching Optimization
```dockerfile
# ❌ Bad: Changes in source code invalidate all layers
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# ✅ Good: Dependencies cached separately from source
FROM node:18-alpine
WORKDIR /app
# Copy dependency files first (changes less frequently)
COPY package*.json ./
RUN npm ci --only=production
# Copy source code last (changes frequently)
COPY . .
RUN npm run build
```
### Security Hardening
```dockerfile
FROM node:18-alpine
# Update packages and remove cache
RUN apk update && apk upgrade && apk add --no-cache dumb-init && rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
# Set ownership and permissions
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --chown=appuser:appgroup . .
# Remove write permissions
RUN chmod -R 555 /app
# Switch to non-root user
USER appuser
# Use dumb-init for proper signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
```
## 2. Image Size Optimization
### Alpine Linux Base Images
```dockerfile
# Size comparison:
# node:18 (Ubuntu-based) ~900MB
# node:18-slim ~200MB
# node:18-alpine ~170MB
FROM node:18-alpine AS base
# Install only necessary packages
RUN apk add --no-cache curl && rm -rf /var/cache/apk/*
```
### .dockerignore Configuration
```dockerignore
# Exclude development files
node_modules
npm-debug.log*
.git
.gitignore
README.md
.env
.env.local
.env.development
.env.test
.nyc_output
coverage
.vscode
.idea
# Exclude build artifacts
dist
build
.next
.nuxt
# Exclude test files
**/*.test.js
**/*.spec.js
test/
tests/
__tests__/
# Exclude documentation
docs/
*.md
!README.md
# OS generated files
.DS_Store
Thumbs.db
```
### Distroless Images for Production
```dockerfile
# For Node.js applications
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Use distroless for final image
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["index.js"]
```
## 3. Docker Compose for Development
### Complete Development Environment
```yaml
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
target: development
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules # Anonymous volume for node_modules
- app-cache:/app/.next # Named volume for Next.js cache
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@db:5432/appdb
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: appdb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d appdb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
networks:
- app-network
volumes:
postgres-data:
redis-data:
app-cache:
networks:
app-network:
driver: bridge
```
### Development Dockerfile
```dockerfile
# Dockerfile.dev
FROM node:18-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Development stage
FROM base AS development
ENV NODE_ENV=development
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Production build stage
FROM base AS builder
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM base AS production
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
COPY --from=builder --chown=nextjs:nodejs /app/dist ./
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
```
## 4. Health Checks and Monitoring
### Application Health Check
```dockerfile
# Add health check to Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD curl -f http://localhost:3000/health || exit 1
```
### Health Check Endpoint
```javascript
// health.js
const express = require('express');
const router = express.Router();
router.get('/health', async (req, res) => {
const healthCheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
environment: process.env.NODE_ENV,
version: process.env.npm_package_version
};
try {
// Check database connection
await checkDatabase();
// Check external services
await checkRedis();
res.status(200).json(healthCheck);
} catch (error) {
healthCheck.message = error.message;
res.status(503).json(healthCheck);
}
});
router.get('/ready', async (req, res) => {
// Readiness probe - check if app is ready to serve traffic
try {
await Promise.all([
checkDatabase(),
checkRedis(),
checkExternalAPIs()
]);
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
module.exports = router;
```
## 5. Environment Configuration
### Environment Variables Management
```bash
# .env.example
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-jwt-secret
API_KEY=your-api-key
# Docker environment file
# .env.docker
NODE_ENV=development
DATABASE_URL=postgresql://user:password@db:5432/appdb
REDIS_URL=redis://redis:6379
```
### Runtime Configuration
```dockerfile
# Use ARG for build-time variables
ARG NODE_ENV=production
ARG BUILD_VERSION
# Use ENV for runtime variables
ENV NODE_ENV=$NODE_ENV
ENV BUILD_VERSION=$BUILD_VERSION
# Don't include secrets in ENV
# Use docker secrets or external secret management
```
## 6. Logging and Debugging
### Structured Logging Configuration
```javascript
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'api',
version: process.env.BUILD_VERSION,
environment: process.env.NODE_ENV
},
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
module.exports = logger;
```
### Debug Container Setup
```dockerfile
# Dockerfile.debug
FROM node:18-alpine
WORKDIR /app
# Install debugging tools
RUN apk add --no-cache curl htop strace tcpdump
# Install nodemon for development
RUN npm install -g nodemon
COPY package*.json ./
RUN npm install
COPY . .
# Expose debug port
EXPOSE 3000 9229
CMD ["nodemon", "--inspect=0.0.0.0:9229", "index.js"]
```
## 7. Security Best Practices
### Secret Management
```yaml
# docker-compose.yml with secrets
version: '3.8'
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true
```
### Network Security
```yaml
# Secure network configuration
version: '3.8'
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend # Only accessible from backend
nginx:
networks:
- frontend
ports:
- "80:80"
- "443:443"
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
```
## 8. Production Deployment
### Production Docker Compose
```yaml
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:\${VERSION:-latest}
restart: unless-stopped
environment:
- NODE_ENV=production
volumes:
- app-logs:/app/logs
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
networks:
- app-network
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.prod.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- app-network
volumes:
app-logs:
networks:
app-network:
external: true
```
### CI/CD Pipeline Integration
```bash
#!/bin/bash
# build-and-deploy.sh
set -e
# Build and tag image
docker build -t myapp:${VERSION} .
docker tag myapp:${VERSION} myapp:latest
# Run security scan
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image myapp:${VERSION}
# Push to registry
docker push myapp:${VERSION}
docker push myapp:latest
# Deploy to production
docker-compose -f docker-compose.prod.yml pull
docker-compose -f docker-compose.prod.yml up -d --remove-orphans
# Health check
sleep 30
curl -f http://localhost/health || exit 1
```
## 9. Performance Optimization
### Resource Limits
```yaml
# docker-compose.yml with resource limits
version: '3.8'
services:
app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
ulimits:
nofile:
soft: 1024
hard: 2048
```
### Caching Strategies
```dockerfile
# Cache npm dependencies globally
FROM node:18-alpine
RUN npm config set cache /tmp/npm-cache
VOLUME ["/tmp/npm-cache"]
# Use BuildKit cache mounts
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --only=production
```
## 10. Monitoring and Observability
### Container Metrics
```yaml
# Add monitoring stack
version: '3.8'
services:
app:
# ... app configuration
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=3000"
- "prometheus.io/path=/metrics"
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
```
## Checklist for Docker Containerization
- [ ] Use multi-stage builds to minimize image size
- [ ] Implement proper layer caching strategies
- [ ] Create non-root user for security
- [ ] Configure .dockerignore for build optimization
- [ ] Add health checks and readiness probes
- [ ] Set up structured logging
- [ ] Implement secret management
- [ ] Configure resource limits and ulimits
- [ ] Use Alpine or distroless base images
- [ ] Set up development docker-compose
- [ ] Configure production deployment
- [ ] Implement container security scanning
- [ ] Add monitoring and observability
- [ ] Document container architecture