Containerization and Docker Practices: Complete Guide from Development to Production

Master Docker basics, image optimization, multi-stage builds, Docker Compose and container orchestration

Containerization and Docker Practices: Complete Guide from Development to Production

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  │                         │
│              └───────────┘                         │
│                                                     │
└─────────────────────────────────────────────────────┘
FeatureContainersVMs
Startup TimeSecondsMinutes
Resource UsageLightweightHeavy
IsolationProcess-levelSystem-level
PerformanceNear-nativeSome overhead
Image SizeMBsGBs

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                      │
│                                                     │
└─────────────────────────────────────────────────────┘
ScenarioRecommended Approach
Local DevelopmentDocker Compose + volumes
CI/CDMulti-stage builds + scanning
ProductionKubernetes / Docker Swarm
MicroservicesOne container per service
Legacy AppsGradual 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.