Docker and Microservices Deployment - Production-Ready Strategies

Muhammad Ubaid Raza
June 18, 2024

Docker and Microservices Deployment: Production-Ready Strategies

Containerization and microservices have revolutionized how we build and deploy applications. After deploying dozens of microservices architectures in production, I'm sharing the battle-tested strategies, patterns, and tools that ensure reliable, scalable deployments.

Docker Fundamentals for Production

Multi-Stage Builds for Optimized Images

# Dockerfile for Node.js application
# Stage 1: Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./
COPY yarn.lock ./

# Install dependencies
RUN yarn install --frozen-lockfile

# Copy source code
COPY . .

# Build application
RUN yarn build

# Stage 2: Production stage
FROM node:18-alpine AS production

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

WORKDIR /app

# Copy built application from builder stage
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 port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# Start application
CMD ["node", "dist/index.js"]

Docker Compose for Local Development

# docker-compose.yml
version: "3.8"

services:
  # API Gateway
  api-gateway:
    build:
      context: ./api-gateway
      dockerfile: Dockerfile.dev
    ports:
      - "8080:8080"
    environment:
      - NODE_ENV=development
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
      - user-service
      - product-service
    volumes:
      - ./api-gateway:/app
      - /app/node_modules
    networks:
      - microservices

  # User Service
  user-service:
    build:
      context: ./user-service
      dockerfile: Dockerfile.dev
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/users
      - JWT_SECRET=dev-secret
    depends_on:
      - postgres
    volumes:
      - ./user-service:/app
      - /app/node_modules
    networks:
      - microservices

  # Product Service
  product-service:
    build:
      context: ./product-service
      dockerfile: Dockerfile.dev
    ports:
      - "3002:3000"
    environment:
      - NODE_ENV=development
      - MONGODB_URL=mongodb://mongo:27017/products
    depends_on:
      - mongo
    volumes:
      - ./product-service:/app
      - /app/node_modules
    networks:
      - microservices

  # Databases
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=users
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - microservices

  mongo:
    image: mongo:6-alpine
    volumes:
      - mongo_data:/data/db
    networks:
      - microservices

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - microservices

  # Monitoring
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - microservices

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - microservices

volumes:
  postgres_data:
  mongo_data:
  redis_data:
  grafana_data:

networks:
  microservices:
    driver: bridge

Kubernetes Deployment Strategies

Deployment Manifests

# user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  labels:
    app: user-service
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
        version: v1
    spec:
      containers:
        - name: user-service
          image: myregistry/user-service:latest
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: user-service-secrets
                  key: database-url
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: user-service-secrets
                  key: jwt-secret
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          volumeMounts:
            - name: config-volume
              mountPath: /app/config
      volumes:
        - name: config-volume
          configMap:
            name: user-service-config
      imagePullSecrets:
        - name: registry-secret

---
apiVersion: v1
kind: Service
metadata:
  name: user-service
  labels:
    app: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 3000
      protocol: TCP
  type: ClusterIP

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: user-service-config
data:
  app.yaml: |
    server:
      port: 3000
      cors:
        enabled: true
        origins: ["https://myapp.com"]
    logging:
      level: info
      format: json

---
apiVersion: v1
kind: Secret
metadata:
  name: user-service-secrets
type: Opaque
data:
  database-url: <base64-encoded-database-url>
  jwt-secret: <base64-encoded-jwt-secret>

Horizontal Pod Autoscaler

# user-service-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: user-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: user-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
        - type: Pods
          value: 4
          periodSeconds: 15
      selectPolicy: Max

Ingress Configuration

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: microservices-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/rate-limit-window: "1m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  tls:
    - hosts:
        - api.myapp.com
      secretName: api-tls-secret
  rules:
    - host: api.myapp.com
      http:
        paths:
          - path: /api/v1/users(/|$)(.*)
            pathType: Prefix
            backend:
              service:
                name: user-service
                port:
                  number: 80
          - path: /api/v1/products(/|$)(.*)
            pathType: Prefix
            backend:
              service:
                name: product-service
                port:
                  number: 80
          - path: /api/v1/orders(/|$)(.*)
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

Service Mesh with Istio

Istio Configuration

# istio-gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: microservices-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: api-tls-secret
      hosts:
        - api.myapp.com

---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: microservices-vs
spec:
  hosts:
    - api.myapp.com
  gateways:
    - microservices-gateway
  http:
    - match:
        - uri:
            prefix: /api/v1/users
      route:
        - destination:
            host: user-service
            port:
              number: 80
      fault:
        delay:
          percentage:
            value: 0.1
          fixedDelay: 5s
      retries:
        attempts: 3
        perTryTimeout: 2s
    - match:
        - uri:
            prefix: /api/v1/products
      route:
        - destination:
            host: product-service
            port:
              number: 80
            subset: v1
          weight: 90
        - destination:
            host: product-service
            port:
              number: 80
            subset: v2
          weight: 10

---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: product-service-dr
spec:
  host: product-service
  trafficPolicy:
    circuitBreaker:
      consecutiveErrors: 3
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
    loadBalancer:
      simple: LEAST_CONN
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

Security Policies

# security-policy.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: user-service-authz
spec:
  selector:
    matchLabels:
      app: user-service
  rules:
    - from:
        - source:
            principals: ["cluster.local/ns/default/sa/api-gateway"]
    - to:
        - operation:
            methods: ["GET", "POST", "PUT", "DELETE"]
            paths: ["/api/v1/users/*"]
    - when:
        - key: request.headers[authorization]
          values: ["Bearer *"]

---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
spec:
  mtls:
    mode: STRICT

CI/CD Pipeline with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy Microservices

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: [user-service, product-service, order-service]

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "yarn"
          cache-dependency-path: ${{ matrix.service }}/yarn.lock

      - name: Install dependencies
        run: |
          cd ${{ matrix.service }}
          yarn install --frozen-lockfile

      - name: Run tests
        run: |
          cd ${{ matrix.service }}
          yarn test:coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ${{ matrix.service }}/coverage/lcov.info
          flags: ${{ matrix.service }}

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    strategy:
      matrix:
        service: [user-service, product-service, order-service]

    steps:
      - uses: actions/checkout@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.service }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix={{branch}}-
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: ./${{ matrix.service }}
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3

      - name: Setup kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: "v1.28.0"

      - name: Configure kubectl
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
          export KUBECONFIG=kubeconfig

      - name: Deploy to Kubernetes
        run: |
          export KUBECONFIG=kubeconfig

          # Update image tags in deployment files
          sed -i "s|image: .*user-service:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/user-service:${{ github.sha }}|" k8s/user-service-deployment.yaml
          sed -i "s|image: .*product-service:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/product-service:${{ github.sha }}|" k8s/product-service-deployment.yaml
          sed -i "s|image: .*order-service:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/order-service:${{ github.sha }}|" k8s/order-service-deployment.yaml

          # Apply deployments
          kubectl apply -f k8s/

          # Wait for rollout
          kubectl rollout status deployment/user-service
          kubectl rollout status deployment/product-service
          kubectl rollout status deployment/order-service

      - name: Run smoke tests
        run: |
          export KUBECONFIG=kubeconfig

          # Wait for services to be ready
          kubectl wait --for=condition=ready pod -l app=user-service --timeout=300s
          kubectl wait --for=condition=ready pod -l app=product-service --timeout=300s
          kubectl wait --for=condition=ready pod -l app=order-service --timeout=300s

          # Run smoke tests
          curl -f https://api.myapp.com/health || exit 1

Monitoring and Observability

Prometheus Configuration

# monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "alert_rules.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - alertmanager:9093

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: "user-service"
    kubernetes_sd_configs:
      - role: endpoints
    relabel_configs:
      - source_labels: [__meta_kubernetes_service_name]
        action: keep
        regex: user-service
      - source_labels: [__meta_kubernetes_endpoint_port_name]
        action: keep
        regex: metrics

  - job_name: "product-service"
    kubernetes_sd_configs:
      - role: endpoints
    relabel_configs:
      - source_labels: [__meta_kubernetes_service_name]
        action: keep
        regex: product-service

  - job_name: "order-service"
    kubernetes_sd_configs:
      - role: endpoints
    relabel_configs:
      - source_labels: [__meta_kubernetes_service_name]
        action: keep
        regex: order-service

  - job_name: "istio-mesh"
    kubernetes_sd_configs:
      - role: endpoints
        namespaces:
          names:
            - istio-system
    relabel_configs:
      - source_labels:
          [__meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
        action: keep
        regex: istio-telemetry;prometheus

Application Metrics

// metrics.js - Express.js middleware for metrics
const promClient = require("prom-client")

// Create a Registry
const register = new promClient.Registry()

// Add default metrics
promClient.collectDefaultMetrics({ register })

// Custom metrics
const httpRequestDuration = new promClient.Histogram({
  name: "http_request_duration_seconds",
  help: "Duration of HTTP requests in seconds",
  labelNames: ["method", "route", "status_code"],
  buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
})

const httpRequestTotal = new promClient.Counter({
  name: "http_requests_total",
  help: "Total number of HTTP requests",
  labelNames: ["method", "route", "status_code"],
})

const activeConnections = new promClient.Gauge({
  name: "active_connections",
  help: "Number of active connections",
})

const databaseQueryDuration = new promClient.Histogram({
  name: "database_query_duration_seconds",
  help: "Duration of database queries in seconds",
  labelNames: ["operation", "table"],
  buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 3, 5],
})

// Register metrics
register.registerMetric(httpRequestDuration)
register.registerMetric(httpRequestTotal)
register.registerMetric(activeConnections)
register.registerMetric(databaseQueryDuration)

// Middleware to track HTTP metrics
const metricsMiddleware = (req, res, next) => {
  const start = Date.now()

  res.on("finish", () => {
    const duration = (Date.now() - start) / 1000
    const route = req.route ? req.route.path : req.path

    httpRequestDuration
      .labels(req.method, route, res.statusCode)
      .observe(duration)

    httpRequestTotal.labels(req.method, route, res.statusCode).inc()
  })

  next()
}

// Database query wrapper
const trackDatabaseQuery = async (operation, table, queryFn) => {
  const start = Date.now()

  try {
    const result = await queryFn()
    const duration = (Date.now() - start) / 1000

    databaseQueryDuration.labels(operation, table).observe(duration)

    return result
  } catch (error) {
    const duration = (Date.now() - start) / 1000

    databaseQueryDuration.labels(operation, table).observe(duration)

    throw error
  }
}

module.exports = {
  register,
  metricsMiddleware,
  trackDatabaseQuery,
  metrics: {
    httpRequestDuration,
    httpRequestTotal,
    activeConnections,
    databaseQueryDuration,
  },
}

Distributed Tracing

// tracing.js - OpenTelemetry setup
const { NodeSDK } = require("@opentelemetry/sdk-node")
const {
  getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node")
const { JaegerExporter } = require("@opentelemetry/exporter-jaeger")
const { Resource } = require("@opentelemetry/resources")
const {
  SemanticResourceAttributes,
} = require("@opentelemetry/semantic-conventions")

const jaegerExporter = new JaegerExporter({
  endpoint: process.env.JAEGER_ENDPOINT || "http://jaeger:14268/api/traces",
})

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]:
      process.env.SERVICE_NAME || "unknown-service",
    [SemanticResourceAttributes.SERVICE_VERSION]:
      process.env.SERVICE_VERSION || "1.0.0",
  }),
  traceExporter: jaegerExporter,
  instrumentations: [
    getNodeAutoInstrumentations({
      "@opentelemetry/instrumentation-fs": {
        enabled: false,
      },
    }),
  ],
})

sdk.start()

// Custom span creation
const { trace } = require("@opentelemetry/api")

const createSpan = (name, fn) => {
  const tracer = trace.getTracer("user-service")

  return tracer.startActiveSpan(name, async (span) => {
    try {
      const result = await fn(span)
      span.setStatus({ code: 1 }) // OK
      return result
    } catch (error) {
      span.setStatus({ code: 2, message: error.message }) // ERROR
      span.recordException(error)
      throw error
    } finally {
      span.end()
    }
  })
}

module.exports = { createSpan }

Security Best Practices

Container Security

# Secure Dockerfile
FROM node:18-alpine AS base

# Install security updates
RUN apk update && apk upgrade && apk add --no-cache dumb-init

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile --production

FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build

FROM base AS runner
WORKDIR /app

# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=deps --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

# Remove unnecessary packages
RUN yarn cache clean

# Set security headers
ENV NODE_OPTIONS="--max-old-space-size=1024"

EXPOSE 3000

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

Network Policies

# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: user-service-netpol
spec:
  podSelector:
    matchLabels:
      app: user-service
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api-gateway
      ports:
        - protocol: TCP
          port: 3000
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to: []
      ports:
        - protocol: TCP
          port: 53
        - protocol: UDP
          port: 53

Conclusion

Successful microservices deployment requires careful attention to:

  1. Container Optimization - Multi-stage builds, security, and resource efficiency
  2. Orchestration - Kubernetes deployments, services, and scaling
  3. Service Mesh - Traffic management, security, and observability
  4. CI/CD Automation - Automated testing, building, and deployment
  5. Monitoring - Comprehensive observability with metrics, logs, and traces
  6. Security - Container security, network policies, and access control

The key is to start simple and gradually add complexity as your system grows. Focus on automation, monitoring, and security from the beginning to ensure your microservices architecture can scale reliably.

Remember: microservices introduce complexity, so make sure the benefits outweigh the costs for your specific use case. Start with a modular monolith and extract services as needed.

What challenges have you faced with microservices deployment? Are there specific aspects of containerization or orchestration you'd like me to dive deeper into? I'm here to help with any Docker or Kubernetes questions you might have.