Skip to content
Vladimir Chavkov
Go back

Docker Containerization: Complete Best Practices Guide

Edit page

Docker Containerization: Complete Best Practices Guide

Docker has revolutionized application deployment by providing a consistent, portable, and efficient way to package and run applications. This comprehensive guide covers Docker best practices for building secure, optimized, and production-ready containers.

Docker Fundamentals

Container vs Virtual Machine

┌─────────────────────────────────────┐
│ Virtual Machine │
├─────────────────────────────────────┤
│ Application Layer │
├─────────────────────────────────────┤
│ Guest Operating System │
├─────────────────────────────────────┤
│ Hypervisor │
├─────────────────────────────────────┤
│ Host Operating System │
├─────────────────────────────────────┤
│ Hardware │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Docker Container │
├─────────────────────────────────────┤
│ Application Layer │
├─────────────────────────────────────┤
│ Binaries/Libraries │
├─────────────────────────────────────┤
│ Docker Engine │
├─────────────────────────────────────┤
│ Host Operating System │
├─────────────────────────────────────┤
│ Hardware │
└─────────────────────────────────────┘

Image Optimization

1. Multi-Stage Builds

Optimized Multi-Stage Dockerfile

# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Copy dependency files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build application
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-w -s -extldflags "-static"' \
-a -installsuffix cgo \
-o main .
# Final stage
FROM scratch AS final
# Copy CA certificates from builder
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=builder /app/main /main
# Use non-root user
USER 65534:65534
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/main", "healthcheck"]
# Set entrypoint
ENTRYPOINT ["/main"]

Node.js Multi-Stage Build

# Dependencies stage
FROM node:18-alpine AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy dependencies
COPY --from=deps /app/node_modules ./node_modules
# Copy source code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM node:18-alpine AS runner
WORKDIR /app
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built application
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Start application
CMD ["node", "server.js"]

2. Layer Optimization

Efficient Layer Caching

# Bad example - breaks layer cache frequently
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip
COPY . /app
RUN pip install -r /app/requirements.txt
CMD ["python3", "/app/app.py"]
# Good example - optimizes layer cache
FROM ubuntu:22.04
# Install dependencies (changes rarely)
RUN apt-get update && apt-get install -y python3 python3-pip && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy requirements first (changes less frequently)
COPY requirements.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# Copy application code (changes frequently)
COPY . /app
# Set working directory
WORKDIR /app
# Run application
CMD ["python3", "app.py"]

.dockerignore for Optimization

# Git
.git
.gitignore
.gitattributes
# Documentation
README.md
docs/
*.md
# Dependencies
node_modules/
__pycache__/
*.pyc
*.pyo
*.pyd
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Environment
.env
.env.local
.env.*.local
# Test
coverage/
.nyc_output/
test/
tests/
*.test.js
*.spec.js
# Build artifacts
dist/
build/
target/
# Temporary files
tmp/
temp/
*.tmp

Security Best Practices

1. Minimal Base Images

Alpine Linux Security

# Use minimal Alpine image
FROM alpine:3.18
# Install security updates and required packages
RUN apk update && \
apk upgrade && \
apk add --no-cache \
ca-certificates \
tzdata \
&& \
rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Set working directory
WORKDIR /app
# Copy application
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Expose port
EXPOSE 8080
# Run application
CMD ["./app"]

Distroless Security

# Use distroless image for maximum security
FROM gcr.io/distroless/static-debian11 AS runtime
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# Copy binary to runtime image
FROM runtime
COPY --from=builder /app/server /server
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/server"]

2. Runtime Security

Read-Only Filesystem

FROM alpine:3.18
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Install application
COPY --chown=appuser:appgroup . /app
WORKDIR /app
# Create temporary directory for write operations
RUN mkdir -p /tmp && \
chown appuser:appgroup /tmp
# Run with read-only filesystem
USER appuser
EXPOSE 8080
HEALTHCHECK CMD wget --spider http://localhost:8080/health
CMD ["./app"]

Docker Compose Security Configuration

version: '3.8'
services:
web:
build: .
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
user: "1001:1001"
environment:
- NODE_ENV=production
networks:
- app-network
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15-alpine
security_opt:
- no-new-privileges:true
environment:
- POSTGRES_DB=app
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
secrets:
- db_password
restart: unless-stopped
networks:
app-network:
driver: bridge
volumes:
postgres_data:
secrets:
db_password:
file: ./secrets/db_password.txt

3. Container Scanning

Trivy Integration

scan-images.sh
#!/bin/bash
# Scan Docker image with Trivy
echo "Scanning Docker image: $1"
# Run Trivy scan
trivy image --format json --output report.json "$1"
# Check for high/critical vulnerabilities
HIGH_VULNS=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL") | .VulnerabilityID' report.json | wc -l)
if [ "$HIGH_VULNS" -gt 0 ]; then
echo "❌ Found $HIGH_VULNS high/critical vulnerabilities"
exit 1
else
echo "✅ No high/critical vulnerabilities found"
exit 0
fi

Docker Security Scan in CI/CD

# GitHub Actions example
name: Docker Security Scan
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker 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'

Production Deployment

1. Environment Configuration

Environment-Specific Configuration

# Multi-environment Dockerfile
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Development environment
FROM base AS development
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Production environment
FROM base AS production
COPY --chown=node:node . .
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "server.js"]

Configuration Management

docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:${VERSION}
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
env_file:
- .env.production
configs:
- source: app_config
target: /app/config/production.json
secrets:
- db_password
- jwt_secret
deploy:
replicas: 3
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
configs:
app_config:
file: ./config/production.json
secrets:
db_password:
external: true
jwt_secret:
external: true

2. Orchestration with Docker Swarm

Docker Stack Configuration

stack.yml
version: '3.8'
services:
web:
image: myapp:${VERSION}
ports:
- "80:3000"
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
monitor: 60s
max_failure_ratio: 0.3
rollback_config:
parallelism: 1
delay: 10s
failure_action: pause
monitor: 60s
max_failure_ratio: 0.3
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
deploy:
replicas: 2
networks:
- app-network
depends_on:
- web
networks:
app-network:
driver: overlay
attachable: true

Rolling Updates

rolling-update.sh
#!/bin/bash
# Deploy new version with rolling update
docker stack deploy -c stack.yml --with-registry-auth myapp
# Monitor service health
echo "Monitoring service health..."
while true; do
HEALTHY=$(docker service ps --format "{{.CurrentState}}" myapp_web | grep -c "Running")
TOTAL=$(docker service ps --format "{{.CurrentState}}" myapp_web | wc -l)
echo "Healthy: $HEALTHY/$TOTAL"
if [ "$HEALTHY" -eq "$TOTAL" ]; then
echo "✅ All replicas are healthy"
break
fi
sleep 10
done

Monitoring and Logging

1. Health Checks

Comprehensive Health Check

FROM python:3.11-slim
# Install health check dependencies
RUN apt-get update && apt-get install -y curl && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Create health check script
COPY healthcheck.py /healthcheck.py
RUN chmod +x /healthcheck.py
# Application health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python /healthcheck.py
EXPOSE 8000
CMD ["python", "app.py"]

Health Check Script

healthcheck.py
#!/usr/bin/env python3
import sys
import requests
import subprocess
import os
def check_application():
"""Check application health"""
try:
response = requests.get('http://localhost:8000/health', timeout=5)
return response.status_code == 200
except:
return False
def check_database():
"""Check database connectivity"""
try:
# Implement database health check
return True
except:
return False
def check_disk_space():
"""Check disk space"""
try:
stat = os.statvfs('/')
free_space = stat.f_bavail * stat.f_frsize
total_space = stat.f_blocks * stat.f_frsize
free_percent = (free_space / total_space) * 100
return free_percent > 10 # At least 10% free space
except:
return False
def main():
"""Main health check"""
checks = [
("Application", check_application),
("Database", check_database),
("Disk Space", check_disk_space),
]
all_healthy = True
for name, check_func in checks:
if not check_func():
print(f"❌ {name} check failed")
all_healthy = False
else:
print(f"✅ {name} check passed")
sys.exit(0 if all_healthy else 1)
if __name__ == "__main__":
main()

2. Logging Configuration

Structured Logging

FROM node:18-alpine
# Create log directory
RUN mkdir -p /app/logs
# Configure logging
ENV NODE_ENV=production
ENV LOG_LEVEL=info
ENV LOG_FORMAT=json
# Volume for logs
VOLUME ["/app/logs"]
# Log rotation configuration
COPY logrotate.conf /etc/logrotate.d/app
# Run logrotate
RUN echo "0 */6 * * * /usr/sbin/logrotate /etc/logrotate.d/app" | crontab -
EXPOSE 3000
CMD ["npm", "start"]

Log Rotation Configuration

Terminal window
# logrotate.conf
/app/logs/*.log {
daily
missingok
rotate 7
compress
delaycompress
notifempty
create 644 nodejs nodejs
postrotate
kill -USR1 $(cat /app/logs/app.pid)
endscript
}

3. Metrics Collection

Prometheus Metrics

FROM prom/prometheus:latest
# Prometheus configuration
COPY prometheus.yml /etc/prometheus/prometheus.yml
COPY rules/ /etc/prometheus/rules/
# Data volume
VOLUME ["/prometheus"]
EXPOSE 9090
CMD ["--config.file=/etc/prometheus/prometheus.yml", \
"--storage.tsdb.path=/prometheus", \
"--web.console.libraries=/etc/prometheus/console_libraries", \
"--web.console.templates=/etc/prometheus/consoles"]

Prometheus Configuration

prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "rules/*.yml"
scrape_configs:
- job_name: 'docker-containers'
static_configs:
- targets: ['localhost:9323']
- job_name: 'myapp'
static_configs:
- targets: ['app:3000']
metrics_path: '/metrics'
scrape_interval: 10s
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093

Performance Optimization

1. Resource Management

Resource Limits

docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
pids: 100
reservations:
cpus: '0.5'
memory: 512M
ulimits:
nofile:
soft: 65536
hard: 65536
sysctls:
- net.core.somaxconn=65535
- net.ipv4.tcp_max_syn_backlog=65535

Performance Tuning

FROM ubuntu:22.04
# System optimization
RUN echo 'net.core.somaxconn = 65535' >> /etc/sysctl.conf && \
echo 'net.ipv4.tcp_max_syn_backlog = 65535' >> /etc/sysctl.conf && \
echo 'net.ipv4.tcp_fin_timeout = 30' >> /etc/sysctl.conf && \
echo 'net.ipv4.tcp_keepalive_time = 1200' >> /etc/sysctl.conf && \
echo 'net.ipv4.tcp_max_tw_buckets = 5000' >> /etc/sysctl.conf
# Application optimization
ENV NODE_OPTIONS="--max-old-space-size=1024"
ENV UV_THREADPOOL_SIZE=128
EXPOSE 3000
CMD ["node", "server.js"]

2. Caching Strategies

Multi-Layer Caching

FROM node:18-alpine
# Build cache layer
FROM node:18-alpine AS cache
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Application layer
FROM cache AS app
COPY . .
RUN npm run build
# Production layer
FROM node:18-alpine AS production
WORKDIR /app
# Copy from cache layer
COPY --from=cache /app/node_modules ./node_modules
COPY --from=app /app/dist ./dist
# Runtime optimization
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=512"
EXPOSE 3000
CMD ["node", "dist/server.js"]

Backup and Recovery

1. Data Persistence

Volume Backup Strategy

backup-volumes.sh
#!/bin/bash
BACKUP_DIR="/backup/$(date +%Y%m%d)"
CONTAINER_NAME="myapp_db"
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Backup database volume
docker run --rm \
-v myapp_postgres_data:/data \
-v "$BACKUP_DIR":/backup \
alpine:latest \
tar czf /backup/postgres_data.tar.gz -C /data .
# Backup application data
docker run --rm \
-v myapp_app_data:/data \
-v "$BACKUP_DIR":/backup \
alpine:latest \
tar czf /backup/app_data.tar.gz -C /data .
# Clean old backups (keep last 7 days)
find /backup -type d -mtime +7 -exec rm -rf {} +
echo "Backup completed: $BACKUP_DIR"

Automated Backup

docker-compose.backup.yml
version: '3.8'
services:
backup:
image: alpine:latest
volumes:
- postgres_data:/data/postgres:ro
- app_data:/data/app:ro
- ./backups:/backup
environment:
- BACKUP_DIR=/backup/$(date +%Y%m%d)
command: >
sh -c "
mkdir -p $$BACKUP_DIR &&
tar czf $$BACKUP_DIR/postgres.tar.gz -C /data/postgres . &&
tar czf $$BACKUP_DIR/app.tar.gz -C /data/app . &&
find /backup -type d -mtime +7 -exec rm -rf {} +
"
deploy:
restart_policy:
condition: none
volumes:
postgres_data:
external: true
app_data:
external: true

2. Disaster Recovery

Recovery Script

recovery.sh
#!/bin/bash
BACKUP_FILE=$1
RESTORE_DIR="/tmp/restore"
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup_file.tar.gz>"
exit 1
fi
# Create restore directory
mkdir -p "$RESTORE_DIR"
# Extract backup
tar xzf "$BACKUP_FILE" -C "$RESTORE_DIR"
# Stop services
docker-compose down
# Restore volumes
docker run --rm \
-v myapp_postgres_data:/data \
-v "$RESTORE_DIR":/backup \
alpine:latest \
tar xzf /backup/postgres_data.tar.gz -C /data
docker run --rm \
-v myapp_app_data:/data \
-v "$RESTORE_DIR":/backup \
alpine:latest \
tar xzf /backup/app_data.tar.gz -C /data
# Start services
docker-compose up -d
# Clean up
rm -rf "$RESTORE_DIR"
echo "Recovery completed from: $BACKUP_FILE"

Best Practices Checklist

Image Building

Security

Production Deployment

Performance

Conclusion

Docker containerization requires careful attention to security, performance, and operational concerns. By following these best practices, you can build robust, secure, and efficient containerized applications that scale effectively in production environments.

Key takeaways:

Remember that containerization is an ongoing process of improvement and optimization. Stay updated with Docker best practices and security recommendations to maintain a secure and efficient container infrastructure.


Edit page
Share this post on:

Previous Post
AWS Cost Optimization Strategies: Complete Guide
Next Post
Python Performance Optimization: Advanced Techniques and Best Practices