Microservices Architecture with Docker and Kubernetes: A Complete Guide


Microservices Architecture with Docker and Kubernetes: A Complete Guide

Microservices architecture has become the standard for building scalable, maintainable applications. This guide will show you how to implement microservices using Docker and Kubernetes.

Understanding Microservices

Microservices architecture breaks down applications into small, independent services:

graph TD
    A[Frontend] --> B[API Gateway]
    B --> C[Auth Service]
    B --> D[User Service]
    B --> E[Product Service]
    B --> F[Order Service]
    D --> G[(User DB)]
    E --> H[(Product DB)]
    F --> I[(Order DB)]

Docker Containerization

Let's containerize a microservice:

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000
CMD ["npm", "start"]

Build and run the container:

# Build image
docker build -t user-service .

# Run container
docker run -p 3000:3000 user-service

Project: E-commerce Microservices

Let's build a complete e-commerce system with microservices:

# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "8000:8000"
    environment:
      - AUTH_SERVICE_URL=http://auth-service:4000
      - USER_SERVICE_URL=http://user-service:3000
      - PRODUCT_SERVICE_URL=http://product-service:5000
      - ORDER_SERVICE_URL=http://order-service:6000
    depends_on:
      - auth-service
      - user-service
      - product-service
      - order-service

  auth-service:
    build: ./auth-service
    ports:
      - "4000:4000"
    environment:
      - MONGODB_URI=mongodb://auth-db:27017/auth
    depends_on:
      - auth-db

  user-service:
    build: ./user-service
    ports:
      - "3000:3000"
    environment:
      - POSTGRES_URI=postgres://user:password@user-db:5432/users
    depends_on:
      - user-db

  product-service:
    build: ./product-service
    ports:
      - "5000:5000"
    environment:
      - MONGODB_URI=mongodb://product-db:27017/products
    depends_on:
      - product-db

  order-service:
    build: ./order-service
    ports:
      - "6000:6000"
    environment:
      - POSTGRES_URI=postgres://user:password@order-db:5432/orders
      - KAFKA_BROKERS=kafka:9092
    depends_on:
      - order-db
      - kafka

  auth-db:
    image: mongo:latest
    ports:
      - "27017:27017"
    volumes:
      - auth-data:/data/db

  user-db:
    image: postgres:latest
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=users
    volumes:
      - user-data:/var/lib/postgresql/data

  product-db:
    image: mongo:latest
    ports:
      - "27018:27017"
    volumes:
      - product-data:/data/db

  order-db:
    image: postgres:latest
    ports:
      - "5433:5432"
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=orders
    volumes:
      - order-data:/var/lib/postgresql/data

  kafka:
    image: confluentinc/cp-kafka:latest
    ports:
      - "9092:9092"
    environment:
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
    depends_on:
      - zookeeper

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    ports:
      - "2181:2181"
    environment:
      - ZOOKEEPER_CLIENT_PORT=2181

volumes:
  auth-data:
  user-data:
  product-data:
  order-data:

API Gateway Service

// api-gateway/src/server.ts
import express from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'

const app = express()

// Auth service proxy
app.use('/auth', createProxyMiddleware({
  target: process.env.AUTH_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/auth': '',
  },
}))

// User service proxy
app.use('/users', createProxyMiddleware({
  target: process.env.USER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/users': '',
  },
}))

// Product service proxy
app.use('/products', createProxyMiddleware({
  target: process.env.PRODUCT_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/products': '',
  },
}))

// Order service proxy
app.use('/orders', createProxyMiddleware({
  target: process.env.ORDER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/orders': '',
  },
}))

app.listen(8000, () => {
  console.log('API Gateway running on port 8000')
})

Auth Service

// auth-service/src/server.ts
import express from 'express'
import jwt from 'jsonwebtoken'
import { connectDB } from './db'

const app = express()
app.use(express.json())

// User authentication
app.post('/login', async (req, res) => {
  const { email, password } = req.body
  
  try {
    const user = await validateCredentials(email, password)
    const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!)
    res.json({ token })
  } catch (error) {
    res.status(401).json({ error: 'Invalid credentials' })
  }
})

// Token validation middleware
export const authMiddleware = (req: any, res: any, next: any) => {
  const token = req.headers.authorization?.split(' ')[1]
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' })
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!)
    req.userId = decoded.userId
    next()
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' })
  }
}

connectDB().then(() => {
  app.listen(4000, () => {
    console.log('Auth service running on port 4000')
  })
})

User Service

// user-service/src/server.ts
import express from 'express'
import { Pool } from 'pg'
import { authMiddleware } from './middleware'

const app = express()
app.use(express.json())

const pool = new Pool({
  connectionString: process.env.POSTGRES_URI
})

// Get user profile
app.get('/profile', authMiddleware, async (req: any, res) => {
  try {
    const result = await pool.query(
      'SELECT * FROM users WHERE id = $1',
      [req.userId]
    )
    
    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'User not found' })
    }
    
    res.json(result.rows[0])
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

// Update user profile
app.put('/profile', authMiddleware, async (req: any, res) => {
  const { name, email } = req.body
  
  try {
    const result = await pool.query(
      'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
      [name, email, req.userId]
    )
    
    res.json(result.rows[0])
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

app.listen(3000, () => {
  console.log('User service running on port 3000')
})

Product Service

// product-service/src/server.ts
import express from 'express'
import { MongoClient, ObjectId } from 'mongodb'
import { authMiddleware } from './middleware'

const app = express()
app.use(express.json())

let db: any

// Connect to MongoDB
MongoClient.connect(process.env.MONGODB_URI!).then((client) => {
  db = client.db()
  console.log('Connected to MongoDB')
})

// Get all products
app.get('/products', async (req, res) => {
  try {
    const products = await db.collection('products').find().toArray()
    res.json(products)
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

// Get product by ID
app.get('/products/:id', async (req, res) => {
  try {
    const product = await db.collection('products').findOne({
      _id: new ObjectId(req.params.id)
    })
    
    if (!product) {
      return res.status(404).json({ error: 'Product not found' })
    }
    
    res.json(product)
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

// Create product (admin only)
app.post('/products', authMiddleware, async (req: any, res) => {
  const { name, price, description } = req.body
  
  try {
    const result = await db.collection('products').insertOne({
      name,
      price,
      description,
      createdAt: new Date()
    })
    
    res.status(201).json({
      _id: result.insertedId,
      name,
      price,
      description
    })
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

app.listen(5000, () => {
  console.log('Product service running on port 5000')
})

Order Service

// order-service/src/server.ts
import express from 'express'
import { Pool } from 'pg'
import { Kafka } from 'kafkajs'
import { authMiddleware } from './middleware'

const app = express()
app.use(express.json())

const pool = new Pool({
  connectionString: process.env.POSTGRES_URI
})

const kafka = new Kafka({
  clientId: 'order-service',
  brokers: process.env.KAFKA_BROKERS!.split(',')
})

const producer = kafka.producer()

// Create order
app.post('/orders', authMiddleware, async (req: any, res) => {
  const { products } = req.body
  
  try {
    // Start transaction
    const client = await pool.connect()
    try {
      await client.query('BEGIN')
      
      // Create order
      const orderResult = await client.query(
        'INSERT INTO orders (user_id, status, created_at) VALUES ($1, $2, $3) RETURNING *',
        [req.userId, 'pending', new Date()]
      )
      
      const order = orderResult.rows[0]
      
      // Add order items
      for (const product of products) {
        await client.query(
          'INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)',
          [order.id, product.id, product.quantity]
        )
      }
      
      await client.query('COMMIT')
      
      // Send order created event
      await producer.send({
        topic: 'order-created',
        messages: [{
          key: order.id.toString(),
          value: JSON.stringify(order)
        }]
      })
      
      res.status(201).json(order)
    } catch (error) {
      await client.query('ROLLBACK')
      throw error
    } finally {
      client.release()
    }
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

// Get user orders
app.get('/orders', authMiddleware, async (req: any, res) => {
  try {
    const result = await pool.query(
      'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
      [req.userId]
    )
    
    res.json(result.rows)
  } catch (error) {
    res.status(500).json({ error: 'Database error' })
  }
})

Promise.all([
  producer.connect(),
  new Promise((resolve) => {
    app.listen(6000, () => {
      console.log('Order service running on port 6000')
      resolve(null)
    })
  })
])

Kubernetes Deployment

Deploy the microservices to Kubernetes:

# kubernetes/api-gateway.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-gateway
  template:
    metadata:
      labels:
        app: api-gateway
    spec:
      containers:
      - name: api-gateway
        image: api-gateway:latest
        ports:
        - containerPort: 8000
        env:
        - name: AUTH_SERVICE_URL
          value: http://auth-service:4000
        - name: USER_SERVICE_URL
          value: http://user-service:3000
        - name: PRODUCT_SERVICE_URL
          value: http://product-service:5000
        - name: ORDER_SERVICE_URL
          value: http://order-service:6000

---
apiVersion: v1
kind: Service
metadata:
  name: api-gateway
spec:
  selector:
    app: api-gateway
  ports:
  - port: 80
    targetPort: 8000
  type: LoadBalancer

Service Discovery and Load Balancing

# kubernetes/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: microservices-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /auth/(.*)
        pathType: Prefix
        backend:
          service:
            name: auth-service
            port:
              number: 80
      - path: /users/(.*)
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 80
      - path: /products/(.*)
        pathType: Prefix
        backend:
          service:
            name: product-service
            port:
              number: 80
      - path: /orders/(.*)
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 80

Horizontal Pod Autoscaling

# kubernetes/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-gateway-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-gateway
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Best Practices

  1. Service Design

    • Keep services small and focused
    • Use domain-driven design
    • Implement proper error handling
    • Design for failure
  2. Communication

    • Use asynchronous communication
    • Implement circuit breakers
    • Handle partial failures
    • Use message queues
  3. Deployment

    • Use CI/CD pipelines
    • Implement blue-green deployment
    • Monitor service health
    • Use proper logging
  4. Security

    • Implement service-to-service auth
    • Use network policies
    • Secure sensitive data
    • Regular security audits

Common Patterns

  1. Circuit Breaker
class CircuitBreaker {
  private failures = 0
  private lastFailure: number = 0
  private readonly threshold = 5
  private readonly timeout = 60000 // 1 minute
  
  async execute(fn: () => Promise<any>) {
    if (this.isOpen()) {
      throw new Error('Circuit breaker is open')
    }
    
    try {
      const result = await fn()
      this.reset()
      return result
    } catch (error) {
      this.recordFailure()
      throw error
    }
  }
  
  private isOpen() {
    if (this.failures >= this.threshold) {
      const timeSinceLastFailure = Date.now() - this.lastFailure
      if (timeSinceLastFailure < this.timeout) {
        return true
      }
      this.reset()
    }
    return false
  }
  
  private recordFailure() {
    this.failures++
    this.lastFailure = Date.now()
  }
  
  private reset() {
    this.failures = 0
    this.lastFailure = 0
  }
}
  1. Service Registry
class ServiceRegistry {
  private services: Map<string, string[]> = new Map()
  
  register(serviceName: string, instance: string) {
    const instances = this.services.get(serviceName) || []
    instances.push(instance)
    this.services.set(serviceName, instances)
  }
  
  unregister(serviceName: string, instance: string) {
    const instances = this.services.get(serviceName) || []
    const index = instances.indexOf(instance)
    if (index !== -1) {
      instances.splice(index, 1)
      this.services.set(serviceName, instances)
    }
  }
  
  getInstance(serviceName: string): string {
    const instances = this.services.get(serviceName) || []
    if (instances.length === 0) {
      throw new Error(`No instances found for ${serviceName}`)
    }
    // Simple round-robin load balancing
    const instance = instances.shift()!
    instances.push(instance)
    return instance
  }
}

Conclusion

Microservices with Docker and Kubernetes provide:

  • Scalable architecture
  • Independent deployment
  • Technology flexibility
  • Fault isolation

Keep exploring patterns and best practices to build robust microservices applications.


Further Reading