Microservices Architecture with Node.js and Docker
Microservices architecture has become the standard for building scalable, maintainable applications. Let's explore how to design and implement a production-ready microservices system using Node.js and Docker.
Why Microservices?
Microservices offer significant advantages over monolithic architecture:
- Scalability - Scale services independently based on demand
- Flexibility - Use different technologies for different services
- Resilience - Failure in one service doesn't crash the entire system
- Faster Development - Teams can work on services independently
- Easy Deployment - Deploy and update services without downtime
- Better Organization - Clear boundaries and responsibilities
Architecture Design Principles
Single Responsibility
Each service should do one thing well.
class="keyword">class="comment">// ❌ Bad: Auth service doing too much
class="keyword">class AuthService {
class="keyword">class="keyword">async login(credentials) {}
class="keyword">class="keyword">async register(userData) {}
class="keyword">class="keyword">async sendEmail(to, subject, body) {}
class="keyword">class="keyword">async processPayment(amount) {}
class="keyword">class="keyword">async generateReport() {}
}
class="keyword">class="comment">// ✅ Good: Separate services
class="keyword">class="comment">// auth-service/
class="keyword">class AuthService {
class="keyword">class="keyword">async login(credentials) {}
class="keyword">class="keyword">async register(userData) {}
class="keyword">class="keyword">async refreshToken(token) {}
}
class="keyword">class="comment">// email-service/
class="keyword">class EmailService {
class="keyword">class="keyword">async sendEmail(to, subject, body) {}
class="keyword">class="keyword">async sendTemplateEmail(to, template, data) {}
}
class="keyword">class="comment">// payment-service/
class="keyword">class PaymentService {
class="keyword">class="keyword">async processPayment(amount, method) {}
class="keyword">class="keyword">async refund(transactionId) {}
}
Service Communication
Choose the right communication pattern.
Synchronous (REST/gRPC)
class="keyword">class="comment">// user-service/routes/users.js
class="keyword">import express class="keyword">from class="keyword">class="string">"express";
class="keyword">import axios class="keyword">from class="keyword">class="string">"axios";
class="keyword">class="keyword">const router = express.Router();
router.get(class="keyword">class="string">"/:id", class="keyword">class="keyword">async (req, res) => {
class="keyword">try {
class="keyword">class="keyword">const user = class="keyword">class="keyword">await User.findById(req.params.id);
class="keyword">class="comment">// Call order service to get user's orders
class="keyword">class="keyword">const ordersResponse = class="keyword">class="keyword">await axios.get(
class="keyword">class="string">http:class="keyword">class="comment">//order-service:3001/orders/user/${user.id},
{
timeout: 5000,
headers: {
Authorization: req.headers.authorization,
},
}
);
res.json({
...user.toJSON(),
orders: ordersResponse.data,
});
} class="keyword">catch (error) {
class="keyword">class="keyword">if (error.code === class="keyword">class="string">"ECONNREFUSED") {
class="keyword">class="comment">// Service unavailable - class="keyword">class="keyword">return partial data
res.json({ ...user.toJSON(), orders: [] });
} class="keyword">class="keyword">else {
res.status(500).json({ error: error.message });
}
}
});
class="keyword">export class="keyword">default router;
Asynchronous (Message Queue)
class="keyword">class="comment">// order-service/events/publisher.js
class="keyword">import amqp class="keyword">from class="keyword">class="string">'amqplib'
class="keyword">class OrderEventPublisher {
constructor() {
class="keyword">this.connection = class="keyword">null
class="keyword">this.channel = class="keyword">null
}
class="keyword">class="keyword">async connect() {
class="keyword">this.connection = class="keyword">class="keyword">await amqp.connect(process.env.RABBITMQ_URL)
class="keyword">this.channel = class="keyword">class="keyword">await class="keyword">this.connection.createChannel()
class="keyword">class="keyword">await class="keyword">this.channel.assertExchange(class="keyword">class="string">'orders', class="keyword">class="string">'topic', { durable: true })
}
class="keyword">class="keyword">async publishOrderCreated(order) {
class="keyword">class="keyword">const message = {
eventType: class="keyword">class="string">'ORDER_CREATED',
timestamp: class="keyword">new Date().toISOString(),
data: {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total
}
}
class="keyword">this.channel.publish(
class="keyword">class="string">'orders',
class="keyword">class="string">'order.created',
Buffer.class="keyword">from(JSON.stringify(message)),
{ persistent: true }
)
console.log(class="keyword">class="string">'Published ORDER_CREATED event:', order.id)
}
}
class="keyword">export class="keyword">default class="keyword">new OrderEventPublisher()
class="keyword">class="comment">// inventory-service/events/consumer.js
class="keyword">import amqp class="keyword">from class="keyword">class="string">'amqplib'
class="keyword">import { reserveStock } class="keyword">from class="keyword">class="string">'../services/inventory'
class="keyword">class OrderEventConsumer {
class="keyword">class="keyword">async connect() {
class="keyword">class="keyword">const connection = class="keyword">class="keyword">await amqp.connect(process.env.RABBITMQ_URL)
class="keyword">class="keyword">const channel = class="keyword">class="keyword">await connection.createChannel()
class="keyword">class="keyword">await channel.assertExchange(class="keyword">class="string">'orders', class="keyword">class="string">'topic', { durable: true })
class="keyword">class="keyword">const queue = class="keyword">class="keyword">await channel.assertQueue(class="keyword">class="string">'inventory-order-events', {
durable: true
})
class="keyword">class="keyword">await channel.bindQueue(queue.queue, class="keyword">class="string">'orders', class="keyword">class="string">'order.created')
channel.consume(queue.queue, class="keyword">class="keyword">async (msg) => {
class="keyword">class="keyword">if (msg) {
class="keyword">class="keyword">const event = JSON.parse(msg.content.toString())
class="keyword">try {
class="keyword">class="keyword">await class="keyword">this.handleOrderCreated(event.data)
channel.ack(msg)
} class="keyword">catch (error) {
console.error(class="keyword">class="string">'Error processing event:', error)
class="keyword">class="comment">// Retry logic or dead letter queue
channel.nack(msg, false, true)
}
}
})
}
class="keyword">class="keyword">async handleOrderCreated(orderData) {
console.log(class="keyword">class="string">'Processing order:', orderData.orderId)
class="keyword">class="keyword">for (class="keyword">class="keyword">const item of orderData.items) {
class="keyword">class="keyword">await reserveStock(item.productId, item.quantity)
}
}
}
class="keyword">export class="keyword">default class="keyword">new OrderEventConsumer()
Service Structure
Standard microservice project structure:
user-service/
├── src/
│ ├── config/
│ │ └── database.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ └── users.js
│ ├── services/
│ │ └── userService.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ ├── events/
│ │ ├── publisher.js
│ │ └── consumer.js
│ └── app.js
├── tests/
│ ├── unit/
│ └── integration/
├── Dockerfile
├── docker-compose.yml
├── package.json
└── .env.example
Docker Containerization
Service Dockerfile
# user-service/Dockerfile
class="keyword">FROM node:20-alpine class="keyword">AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Production stage
class="keyword">FROM node:20-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Copy class="keyword">from builder
COPY --class="keyword">from=builder --chown=nodejs:nodejs /app /app
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e class="keyword">class="string">"require('http').get('http:class="keyword">class="comment">//localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start application
CMD [class="keyword">class="string">"node", class="keyword">class="string">"src/app.js"]
Docker Compose for Development
# docker-compose.yml
version: class="keyword">class="string">"3.8"
services:
# API Gateway
api-gateway:
build:
context: ./api-gateway
dockerfile: Dockerfile
ports:
- class="keyword">class="string">"3000:3000"
environment:
- NODE_ENV=development
- USER_SERVICE_URL=http:class="keyword">class="comment">//user-service:3001
- ORDER_SERVICE_URL=http:class="keyword">class="comment">//order-service:3002
- PRODUCT_SERVICE_URL=http:class="keyword">class="comment">//product-service:3003
depends_on:
- user-service
- order-service
- product-service
networks:
- microservices
# User Service
user-service:
build:
context: ./user-service
dockerfile: Dockerfile
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql:class="keyword">class="comment">//postgres:password@user-db:5432/users
- RABBITMQ_URL=amqp:class="keyword">class="comment">//rabbitmq:5672
- JWT_SECRET=${JWT_SECRET}
depends_on:
- user-db
- rabbitmq
networks:
- microservices
user-db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=users
- POSTGRES_PASSWORD=password
volumes:
- user-data:/class="keyword">class="keyword">var/lib/postgresql/data
networks:
- microservices
# Order Service
order-service:
build:
context: ./order-service
dockerfile: Dockerfile
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql:class="keyword">class="comment">//postgres:password@order-db:5432/orders
- RABBITMQ_URL=amqp:class="keyword">class="comment">//rabbitmq:5672
depends_on:
- order-db
- rabbitmq
networks:
- microservices
order-db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=orders
- POSTGRES_PASSWORD=password
volumes:
- order-data:/class="keyword">class="keyword">var/lib/postgresql/data
networks:
- microservices
# Product Service
product-service:
build:
context: ./product-service
dockerfile: Dockerfile
environment:
- NODE_ENV=development
- MONGODB_URL=mongodb:class="keyword">class="comment">//product-db:27017/products
- REDIS_URL=redis:class="keyword">class="comment">//redis:6379
depends_on:
- product-db
- redis
networks:
- microservices
product-db:
image: mongo:7
volumes:
- product-data:/data/db
networks:
- microservices
# Message Broker
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- class="keyword">class="string">"5672:5672"
- class="keyword">class="string">"15672:15672"
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
volumes:
- rabbitmq-data:/class="keyword">class="keyword">var/lib/rabbitmq
networks:
- microservices
# Cache
redis:
image: redis:7-alpine
ports:
- class="keyword">class="string">"6379:6379"
volumes:
- redis-data:/data
networks:
- microservices
volumes:
user-data:
order-data:
product-data:
rabbitmq-data:
redis-data:
networks:
microservices:
driver: bridge
API Gateway Pattern
Centralize routing and cross-cutting concerns.
class="keyword">class="comment">// api-gateway/src/app.js
class="keyword">import express class="keyword">from class="keyword">class="string">"express";
class="keyword">import { createProxyMiddleware } class="keyword">from class="keyword">class="string">"http-proxy-middleware";
class="keyword">import rateLimit class="keyword">from class="keyword">class="string">"express-rate-limit";
class="keyword">import helmet class="keyword">from class="keyword">class="string">"helmet";
class="keyword">import cors class="keyword">from class="keyword">class="string">"cors";
class="keyword">import jwt class="keyword">from class="keyword">class="string">"jsonwebtoken";
class="keyword">class="keyword">const app = express();
class="keyword">class="comment">// Security
app.use(helmet());
app.use(cors());
class="keyword">class="comment">// Rate limiting
class="keyword">class="keyword">const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
});
app.use(limiter);
class="keyword">class="comment">// Authentication middleware
class="keyword">class="keyword">const authenticate = (req, res, next) => {
class="keyword">class="keyword">const token = req.headers.authorization?.split(class="keyword">class="string">" ")[1];
class="keyword">class="keyword">if (!token) {
class="keyword">class="keyword">return res.status(401).json({ error: class="keyword">class="string">"No token provided" });
}
class="keyword">try {
class="keyword">class="keyword">const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} class="keyword">catch (error) {
res.status(401).json({ error: class="keyword">class="string">"Invalid token" });
}
};
class="keyword">class="comment">// Route to services
app.use(
class="keyword">class="string">"/api/users",
createProxyMiddleware({
target: process.env.USER_SERVICE_URL,
changeOrigin: true,
pathRewrite: { class="keyword">class="string">"^/api/users": class="keyword">class="string">"" },
onProxyReq: (proxyReq, req) => {
class="keyword">class="comment">// Forward user info
class="keyword">class="keyword">if (req.user) {
proxyReq.setHeader(class="keyword">class="string">"X-User-Id", req.user.id);
proxyReq.setHeader(class="keyword">class="string">"X-User-Role", req.user.role);
}
},
})
);
app.use(
class="keyword">class="string">"/api/orders",
authenticate,
createProxyMiddleware({
target: process.env.ORDER_SERVICE_URL,
changeOrigin: true,
pathRewrite: { class="keyword">class="string">"^/api/orders": class="keyword">class="string">"" },
})
);
app.use(
class="keyword">class="string">"/api/products",
createProxyMiddleware({
target: process.env.PRODUCT_SERVICE_URL,
changeOrigin: true,
pathRewrite: { class="keyword">class="string">"^/api/products": class="keyword">class="string">"" },
})
);
class="keyword">class="comment">// Health check
app.get(class="keyword">class="string">"/health", (req, res) => {
res.json({ status: class="keyword">class="string">"healthy" });
});
class="keyword">class="keyword">const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(class="keyword">class="string">API Gateway running on port ${PORT});
});
Service Discovery
Implement service registry for dynamic discovery.
class="keyword">class="comment">// service-registry/index.js
class="keyword">import express class="keyword">from class="keyword">class="string">"express";
class="keyword">class="keyword">const app = express();
app.use(express.json());
class="keyword">class="keyword">const services = class="keyword">new Map();
class="keyword">class="comment">// Register service
app.post(class="keyword">class="string">"/register", (req, res) => {
class="keyword">class="keyword">const { name, host, port, healthCheck } = req.body;
class="keyword">class="keyword">const serviceId = class="keyword">class="string">${name}-${Date.now()};
services.set(serviceId, {
id: serviceId,
name,
url: class="keyword">class="string">http:class="keyword">class="comment">//${host}:${port},
healthCheck,
registeredAt: class="keyword">new Date(),
lastHeartbeat: class="keyword">new Date(),
});
res.json({ serviceId });
});
class="keyword">class="comment">// Heartbeat
app.post(class="keyword">class="string">"/heartbeat/:serviceId", (req, res) => {
class="keyword">class="keyword">const service = services.get(req.params.serviceId);
class="keyword">class="keyword">if (service) {
service.lastHeartbeat = class="keyword">new Date();
res.json({ status: class="keyword">class="string">"ok" });
} class="keyword">class="keyword">else {
res.status(404).json({ error: class="keyword">class="string">"Service not found" });
}
});
class="keyword">class="comment">// Discover services
app.get(class="keyword">class="string">"/discover/:serviceName", (req, res) => {
class="keyword">class="keyword">const instances = Array.class="keyword">from(services.values())
.filter((s) => s.name === req.params.serviceName)
.filter((s) => {
class="keyword">class="keyword">const timeSinceHeartbeat = Date.now() - s.lastHeartbeat.getTime();
class="keyword">class="keyword">return timeSinceHeartbeat < 30000; class="keyword">class="comment">// 30 seconds
});
class="keyword">class="keyword">if (instances.length === 0) {
class="keyword">class="keyword">return res.status(404).json({ error: class="keyword">class="string">"No healthy instances" });
}
class="keyword">class="comment">// Round-robin load balancing
class="keyword">class="keyword">const instance = instances[Math.floor(Math.random() * instances.length)];
res.json(instance);
});
app.listen(8500, () => {
console.log(class="keyword">class="string">"Service registry running on port 8500");
});
Kubernetes Deployment
Deploy to production with Kubernetes.
# user-service/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: your-registry/user-service:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: class="keyword">class="string">"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: class="keyword">class="string">"256Mi"
cpu: class="keyword">class="string">"250m"
limits:
memory: class="keyword">class="string">"512Mi"
cpu: class="keyword">class="string">"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
spec:
selector:
app: user-service
ports:
- protocol: TCP
port: 80
targetPort: 3000
class="keyword">type: ClusterIP
---
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:
- class="keyword">type: Resource
resource:
name: cpu
target:
class="keyword">type: Utilization
averageUtilization: 70
Monitoring and Observability
Track service health and performance.
class="keyword">class="comment">// monitoring/prometheus.js
class="keyword">import client class="keyword">from class="keyword">class="string">"prom-client";
class="keyword">class="keyword">const register = class="keyword">new client.Registry();
class="keyword">class="comment">// Default metrics
client.collectDefaultMetrics({ register });
class="keyword">class="comment">// Custom metrics
class="keyword">class="keyword">const httpRequestDuration = class="keyword">new client.Histogram({
name: class="keyword">class="string">"http_request_duration_seconds",
help: class="keyword">class="string">"Duration of HTTP requests in seconds",
labelNames: [class="keyword">class="string">"method", class="keyword">class="string">"route", class="keyword">class="string">"status_code"],
buckets: [0.1, 0.5, 1, 2, 5],
});
class="keyword">class="keyword">const httpRequestTotal = class="keyword">new client.Counter({
name: class="keyword">class="string">"http_requests_total",
help: class="keyword">class="string">"Total number of HTTP requests",
labelNames: [class="keyword">class="string">"method", class="keyword">class="string">"route", class="keyword">class="string">"status_code"],
});
register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
class="keyword">class="comment">// Middleware
class="keyword">export class="keyword">class="keyword">const metricsMiddleware = (req, res, next) => {
class="keyword">class="keyword">const start = Date.now();
res.on(class="keyword">class="string">"finish", () => {
class="keyword">class="keyword">const duration = (Date.now() - start) / 1000;
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, req.route?.path || req.path, res.statusCode)
.inc();
});
next();
};
class="keyword">export { register };
Best Practices
1. Database per Service
2. API Versioning
3. Circuit Breaker Pattern
4. Distributed Tracing
5. Centralized Logging
6. Automated Testing
7. CI/CD Pipeline
8. Security (mTLS, API Keys)
Conclusion
Microservices architecture enables building scalable, resilient systems. Start with a solid foundation: proper service boundaries, containerization, orchestration, and observability. Remember: microservices add complexity, so only adopt them when the benefits outweigh the costs.
Happy architecting! 🏗️