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
-
Service Design
- Keep services small and focused
- Use domain-driven design
- Implement proper error handling
- Design for failure
-
Communication
- Use asynchronous communication
- Implement circuit breakers
- Handle partial failures
- Use message queues
-
Deployment
- Use CI/CD pipelines
- Implement blue-green deployment
- Monitor service health
- Use proper logging
-
Security
- Implement service-to-service auth
- Use network policies
- Secure sensitive data
- Regular security audits
Common Patterns
- 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
}
}
- 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.