Containerization has become the standard for modern application deployment. This article explores Docker core concepts and best practices.
Container Technology Basics
Containers vs Virtual Machines
Containers vs Virtual Machines:
┌─────────────────────────────────────────────────────┐
│ │
│ Virtual Machines (VM) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ App A │ │ App B │ │ App C │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │Guest OS │ │Guest OS │ │Guest OS │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └───────────┼───────────┘ │
│ ┌─────┴─────┐ │
│ │ Hypervisor│ │
│ ├───────────┤ │
│ │ Host OS │ │
│ └───────────┘ │
│ │
│ Containers │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ App A │ │ App B │ │ App C │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └───────────┼───────────┘ │
│ ┌─────┴─────┐ │
│ │ Docker │ │
│ ├───────────┤ │
│ │ Host OS │ │
│ └───────────┘ │
│ │
└─────────────────────────────────────────────────────┘
| Feature | Containers | VMs |
|---|---|---|
| Startup Time | Seconds | Minutes |
| Resource Usage | Lightweight | Heavy |
| Isolation | Process-level | System-level |
| Performance | Near-native | Some overhead |
| Image Size | MBs | GBs |
Writing Dockerfiles
Basic Example
# Node.js Application Dockerfile
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy dependency files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build application
RUN npm run build
# Expose port
EXPOSE 3000
# Startup command
CMD ["node", "dist/index.js"]
Multi-Stage Builds
# Multi-stage build for optimized image size
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production image
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 build artifacts from builder stage
COPY --from=builder /app/dist ./dist
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Go Application Example
# Go multi-stage build
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copy dependency files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build static binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Minimal production image
FROM scratch
# Copy binary
COPY --from=builder /app/main /main
# Copy CA certificates (for HTTPS requests)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/main"]
Image Optimization
Layer Caching
# Before: Reinstalls dependencies on every code change
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# After: Leveraging layer cache
FROM node:20-alpine
WORKDIR /app
# Copy dependency files first (changes less frequently)
COPY package*.json ./
RUN npm ci
# Then copy source code (changes more frequently)
COPY . .
RUN npm run build
.dockerignore Configuration
# Version control
.git
.gitignore
# Dependencies
node_modules
vendor
# Build artifacts
dist
build
*.log
# Development files
.env.local
.env.development
*.md
!README.md
# IDE
.vscode
.idea
*.swp
# Testing
coverage
__tests__
*.test.js
*.spec.js
# Docker
Dockerfile*
docker-compose*
.dockerignore
Image Size Comparison
# Different base image sizes
# node:20 ~1GB
# node:20-slim ~200MB
# node:20-alpine ~130MB
# Recommended: Use Alpine images
FROM node:20-alpine
# If additional tools needed
RUN apk add --no-cache \
curl \
tzdata
Docker Compose
Development Environment
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
command: npm run dev
db:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
adminer:
image: adminer
ports:
- "8080:8080"
depends_on:
- db
volumes:
postgres_data:
redis_data:
Production Environment
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:${VERSION:-latest}
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
ports:
- "3000:3000"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
Multi-Environment Configuration
# docker-compose.override.yml (auto-loaded in development)
version: '3.8'
services:
app:
build:
context: .
target: development
volumes:
- .:/app:cached
- /app/node_modules
environment:
- DEBUG=true
# Start with different configurations
# Development
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Container Security
Security Best Practices
# Security-optimized Dockerfile
FROM node:20-alpine
# Update system packages
RUN apk update && apk upgrade
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
WORKDIR /app
# Set directory permissions
RUN chown -R appuser:appgroup /app
# Copy dependencies and install
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
# Copy application code
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
# Read-only filesystem
# Use --read-only flag at runtime
EXPOSE 3000
CMD ["node", "index.js"]
Image Scanning
# Scan image for vulnerabilities with Trivy
trivy image myapp:latest
# Use Docker Scout
docker scout cves myapp:latest
# Integrate scanning in CI/CD
# .github/workflows/security.yml
name: Security Scan
on:
push:
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'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Network Configuration
Custom Networks
version: '3.8'
services:
frontend:
networks:
- frontend-network
backend:
networks:
- frontend-network
- backend-network
database:
networks:
- backend-network
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
internal: true # No external access
Service Discovery
// In Docker networks, service names work as hostnames
const dbHost = process.env.DATABASE_HOST || 'db';
const redisHost = process.env.REDIS_HOST || 'cache';
// Docker Compose service names auto-resolve to container IPs
const databaseUrl = `postgresql://user:pass@${dbHost}:5432/myapp`;
Health Checks
# Health check in Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
// Health check endpoint
app.get('/health', (req, res) => {
const healthcheck = {
uptime: process.uptime(),
status: 'OK',
timestamp: Date.now(),
};
try {
// Check database connection
await db.query('SELECT 1');
res.status(200).json(healthcheck);
} catch (error) {
healthcheck.status = 'ERROR';
res.status(503).json(healthcheck);
}
});
Log Management
# Logging configuration
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels: "production"
env: "NODE_ENV"
# Or use centralized logging
fluentd:
image: fluent/fluentd:v1.14
volumes:
- ./fluentd/conf:/fluentd/etc
ports:
- "24224:24224"
app-with-fluentd:
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "docker.{{.Name}}"
Best Practices Summary
Docker Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Image Building │
│ ├── Use multi-stage builds │
│ ├── Choose appropriate base images │
│ ├── Optimize layer caching │
│ └── Use .dockerignore │
│ │
│ Security │
│ ├── Use non-root users │
│ ├── Regularly scan for vulnerabilities │
│ ├── Minimize image contents │
│ └── Use fixed version tags │
│ │
│ Operations │
│ ├── Configure health checks │
│ ├── Set reasonable resource limits │
│ ├── Centralized log management │
│ └── Use environment variables │
│ │
│ Developer Experience │
│ ├── Use Docker Compose │
│ ├── Hot reload support │
│ ├── Separate dev/prod configs │
│ └── Document build process │
│ │
└─────────────────────────────────────────────────────┘
| Scenario | Recommended Approach |
|---|---|
| Local Development | Docker Compose + volumes |
| CI/CD | Multi-stage builds + scanning |
| Production | Kubernetes / Docker Swarm |
| Microservices | One container per service |
| Legacy Apps | Gradual containerization |
Containerization is not just about packaging apps, it’s the foundation of modern operations. Master Docker to make deployments simple and reliable.
Build once, run anywhere. Containerization makes application delivery standardized and repeatable.