← Back to Articles

Building Scalable Microservices with Docker and Kubernetes: A Complete Guide

Learn how to architect, deploy, and manage containerized microservices at scale using Docker, Kubernetes, and modern DevOps practices.

By Urban M.
DockerKubernetesDevOpsMicroservicesCloud
Building Scalable Microservices with Docker and Kubernetes: A Complete Guide

Building Scalable Microservices with Docker and Kubernetes

In today's cloud-native world, containerization and orchestration are essential skills for any backend developer. This comprehensive guide will walk you through building production-ready microservices.

Kubernetes Architecture


Why Microservices?

Traditional monolithic applications face several challenges at scale:

MonolithicMicroservices
❌ Single point of failure✅ Isolated failures
❌ Difficult to scale✅ Independent scaling
❌ Long deployment cycles✅ Rapid deployments
❌ Technology lock-in✅ Technology flexibility
❌ Complex codebase✅ Smaller, focused services

Architecture Overview

Microservice Communication Patterns

Rendering diagram...

Key Components:

  1. API Gateway - Single entry point for clients
  2. Services - Independent, loosely coupled
  3. Databases - Each service owns its data
  4. Message Queue - Async communication

Containerization with Docker

Creating an Optimized Dockerfile

Here's a production-ready multi-stage Dockerfile for a Node.js service:

# Stage 1: Dependencies
FROM node:20-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production

# Stage 3: Production
FROM node:20-alpine AS production
WORKDIR /app

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

# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

# Security best practices
USER nodejs
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"

CMD ["node", "dist/main.js"]

Docker Compose for Local Development

version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
      - user-service
      - order-service
    networks:
      - microservices

  user-service:
    build: ./user-service
    environment:
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/users
    depends_on:
      - postgres
    networks:
      - microservices

  order-service:
    build: ./order-service
    environment:
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/orders
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq
    networks:
      - microservices

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - microservices

  redis:
    image: redis:7-alpine
    networks:
      - microservices

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "15672:15672"
    networks:
      - microservices

networks:
  microservices:
    driver: bridge

volumes:
  postgres-data:

Kubernetes Deployment

Service Deployment Manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: production
  labels:
    app: user-service
    version: v1
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
        version: v1
    spec:
      containers:
      - name: user-service
        image: myregistry/user-service:v1.2.3
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: user-service-secrets
              key: database-url
        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
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 3000
    protocol: TCP
    name: http

Horizontal Pod Autoscaler

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: user-service-hpa
  namespace: production
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: 50
        periodSeconds: 15
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 2
        periodSeconds: 15
      selectPolicy: Max

Implementing Service Mesh with Istio

Why Service Mesh?

A service mesh provides observability, security, and traffic management without modifying application code.

Benefits:

  • 🔐 mTLS between services
  • 📊 Distributed tracing
  • 🚦 Traffic splitting (A/B testing, canary deployments)
  • 🔄 Circuit breaking and retries
  • 📈 Metrics collection

Istio Virtual Service for Canary Deployment

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: user-service
spec:
  hosts:
  - user-service
  http:
  - match:
    - headers:
        x-beta-user:
          exact: "true"
    route:
    - destination:
        host: user-service
        subset: v2
      weight: 100
  - route:
    - destination:
        host: user-service
        subset: v1
      weight: 90
    - destination:
        host: user-service
        subset: v2
      weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: user-service
spec:
  host: user-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        maxRequestsPerConnection: 2
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

Monitoring and Observability

Prometheus Metrics

Instrument your services with Prometheus metrics:

import { register, Counter, Histogram } from 'prom-client';

// Request counter
const requestCounter = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status'],
});

// Request duration
const requestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'path'],
  buckets: [0.1, 0.5, 1, 2, 5],
});

// Middleware
app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    
    requestCounter.inc({
      method: req.method,
      path: req.route?.path || req.path,
      status: res.statusCode,
    });
    
    requestDuration.observe(
      {
        method: req.method,
        path: req.route?.path || req.path,
      },
      duration
    );
  });
  
  next();
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.send(await register.metrics());
});

Grafana Dashboard Configuration

{
  "dashboard": {
    "title": "User Service Metrics",
    "panels": [
      {
        "title": "Request Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total{service=\"user-service\"}[5m])"
          }
        ]
      },
      {
        "title": "95th Percentile Latency",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))"
          }
        ]
      },
      {
        "title": "Error Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total{service=\"user-service\",status=~\"5..\"}[5m])"
          }
        ]
      }
    ]
  }
}

CI/CD Pipeline

GitHub Actions Workflow

name: Deploy Microservice

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Login to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
      
      - name: Set up kubectl
        uses: azure/setup-kubectl@v3
      
      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/user-service \
            user-service=ghcr.io/${{ github.repository }}:${{ github.sha }} \
            --record
          kubectl rollout status deployment/user-service
        env:
          KUBECONFIG: ${{ secrets.KUBECONFIG }}

Best Practices Checklist

✅ Security

  • Use non-root users in containers
  • Scan images for vulnerabilities
  • Implement mTLS between services
  • Store secrets in vault/secret manager
  • Enable network policies

✅ Performance

  • Implement caching strategies
  • Use connection pooling
  • Enable horizontal pod autoscaling
  • Optimize Docker images (multi-stage builds)
  • Implement rate limiting

✅ Observability

  • Collect metrics (Prometheus)
  • Implement distributed tracing (Jaeger/Zipkin)
  • Centralized logging (ELK/Loki)
  • Set up alerting (AlertManager)
  • Create dashboards (Grafana)

✅ Reliability

  • Implement health checks
  • Configure resource limits
  • Set up circuit breakers
  • Implement retry logic
  • Use graceful shutdown

Conclusion

Building scalable microservices requires careful planning and the right tools. By leveraging Docker for containerization and Kubernetes for orchestration, you can create systems that are:

🚀 Scalable - Handle millions of requests
🔒 Secure - Protected by default
📊 Observable - Full visibility into system behavior
🔄 Resilient - Self-healing and fault-tolerant

Next Steps

  1. Start small - Convert one service at a time
  2. Automate everything - CI/CD is essential
  3. Monitor from day one - Don't wait for problems
  4. Learn from failures - Implement chaos engineering

Ready to modernize your infrastructure? Our team specializes in cloud-native architectures and can help you migrate to microservices. Schedule a consultation today!

Official Resources

Resources: