DevOps February 15, 2024 12 min read

Microservices Architecture with Docker

A practical guide to building and deploying microservices using Docker and Node.js. Learn about containerization and orchestration.

Nikesh Bhattarai
Nikesh Bhattarai
Backend Developer & AI/ML Engineer
Microservices Architecture

Introduction

Microservices architecture has revolutionized how we build and deploy applications. By breaking down monolithic applications into smaller, independent services, we can achieve better scalability, maintainability, and deployment flexibility. Combined with Docker's containerization technology, microservices become even more powerful and manageable.

This comprehensive guide will walk you through building a complete microservices architecture using Node.js and Docker, covering everything from design principles to deployment strategies and best practices.

Understanding Microservices

Microservices are an architectural style that structures an application as a collection of loosely coupled, independently deployable services. Each service is responsible for a specific business capability and can be developed, deployed, and scaled independently.

// Monolithic vs Microservices Comparison

// Monolithic Application Structure
const app = express();

// All functionality in one application
app.get('/users', handleUsers);
app.get('/orders', handleOrders);
app.get('/products', handleProducts);
app.get('/payments', handlePayments);
app.get('/notifications', handleNotifications);

// Single database, single deployment unit
app.listen(3000);

// Microservices Architecture
// User Service (Port 3001)
const userApp = express();
userApp.get('/users', handleUsers);
userApp.listen(3001);

// Order Service (Port 3002)
const orderApp = express();
orderApp.get('/orders', handleOrders);
orderApp.listen(3002);

// Product Service (Port 3003)
const productApp = express();
productApp.get('/products', handleProducts);
productApp.listen(3003);

// Each service has its own database and deployment lifecycle

Microservices Design Principles

1. Single Responsibility Principle

Each service should focus on one business capability:

// User Service - Only handles user-related operations
class UserService {
  async createUser(userData) {
    // User creation logic
    const user = await this.userRepository.create(userData);
    
    // Publish event for other services
    await this.eventBus.publish('user.created', { userId: user.id });
    
    return user;
  }
  
  async getUserById(id) {
    return await this.userRepository.findById(id);
  }
  
  async updateUser(id, userData) {
    return await this.userRepository.update(id, userData);
  }
}

// Order Service - Only handles order-related operations
class OrderService {
  async createOrder(orderData) {
    // Validate user exists
    const user = await this.userService.getUserById(orderData.userId);
    if (!user) {
      throw new Error('User not found');
    }
    
    // Create order
    const order = await this.orderRepository.create(orderData);
    
    // Publish events
    await this.eventBus.publish('order.created', { orderId: order.id });
    await this.eventBus.publish('notification.order', { 
      userId: order.userId, 
      orderId: order.id 
    });
    
    return order;
  }
}

2. Database per Service

Each service should have its own database:

// User Service Database Configuration
const userDbConfig = {
  host: process.env.USER_DB_HOST,
  database: 'user_service_db',
  username: process.env.USER_DB_USER,
  password: process.env.USER_DB_PASSWORD
};

// Order Service Database Configuration
const orderDbConfig = {
  host: process.env.ORDER_DB_HOST,
  database: 'order_service_db',
  username: process.env.ORDER_DB_USER,
  password: process.env.ORDER_DB_PASSWORD
};

// Service-specific database connection
class DatabaseConnection {
  constructor(config) {
    this.config = config;
    this.connection = null;
  }
  
  async connect() {
    this.connection = await mysql.createConnection(this.config);
    return this.connection;
  }
  
  async query(sql, params) {
    if (!this.connection) {
      await this.connect();
    }
    return await this.connection.query(sql, params);
  }
}

// Usage in services
const userService = new UserService(new DatabaseConnection(userDbConfig));
const orderService = new OrderService(new DatabaseConnection(orderDbConfig));

Docker Containerization

Creating Dockerfiles

Containerize each microservice:

# User Service Dockerfile
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

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

# Change ownership of the app directory
RUN chown -R nodejs:nodejs /app
USER nodejs

# Expose port
EXPOSE 3001

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

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

# Order Service Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
RUN chown -R nodejs:nodejs /app
USER nodejs

EXPOSE 3002

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

CMD ["node", "src/index.js"]

Multi-Stage Dockerfiles

Optimize image size with multi-stage builds:

# Multi-stage Dockerfile for smaller production images

# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Copy all files for building
COPY package*.json ./
COPY tsconfig.json ./
COPY src/ ./src/

# Install all dependencies including dev dependencies
RUN npm ci

# Build the application
RUN npm run build

# Production stage
FROM node:18-alpine AS production

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy built application from builder stage
COPY --from=builder /app/dist ./dist

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

# Change ownership
RUN chown -R nodejs:nodejs /app
USER nodejs

# Expose port
EXPOSE 3001

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node dist/healthcheck.js || exit 1

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

Docker Compose for Local Development

Use Docker Compose to orchestrate multiple services:

# docker-compose.yml
version: '3.8'

services:
  # API Gateway
  api-gateway:
    build: ./services/api-gateway
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - USER_SERVICE_URL=http://user-service:3001
      - ORDER_SERVICE_URL=http://order-service:3002
      - PRODUCT_SERVICE_URL=http://product-service:3003
    depends_on:
      - user-service
      - order-service
      - product-service
    networks:
      - microservices

  # User Service
  user-service:
    build: ./services/user-service
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=development
      - DB_HOST=user-db
      - DB_PORT=5432
      - DB_NAME=user_service_db
      - DB_USER=postgres
      - DB_PASSWORD=password
    depends_on:
      - user-db
    networks:
      - microservices

  # Order Service
  order-service:
    build: ./services/order-service
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=development
      - DB_HOST=order-db
      - DB_PORT=5432
      - DB_NAME=order_service_db
      - DB_USER=postgres
      - DB_PASSWORD=password
      - REDIS_URL=redis://redis:6379
    depends_on:
      - order-db
      - redis
    networks:
      - microservices

  # Product Service
  product-service:
    build: ./services/product-service
    ports:
      - "3003:3003"
    environment:
      - NODE_ENV=development
      - DB_HOST=product-db
      - DB_PORT=5432
      - DB_NAME=product_service_db
      - DB_USER=postgres
      - DB_PASSWORD=password
    depends_on:
      - product-db
    networks:
      - microservices

  # Notification Service
  notification-service:
    build: ./services/notification-service
    ports:
      - "3004:3004"
    environment:
      - NODE_ENV=development
      - SMTP_HOST=smtp.gmail.com
      - SMTP_PORT=587
      - SMTP_USER=${SMTP_USER}
      - SMTP_PASS=${SMTP_PASS}
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
    networks:
      - microservices

  # Databases
  user-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=user_service_db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - user_db_data:/var/lib/postgresql/data
    networks:
      - microservices

  order-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=order_service_db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - order_db_data:/var/lib/postgresql/data
    networks:
      - microservices

  product-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=product_service_db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - product_db_data:/var/lib/postgresql/data
    networks:
      - microservices

  # Redis for caching and messaging
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - microservices

  # Message Broker (RabbitMQ)
  message-broker:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=password
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - microservices

volumes:
  user_db_data:
  order_db_data:
  product_db_data:
  redis_data:
  rabbitmq_data:

networks:
  microservices:
    driver: bridge

Inter-Service Communication

HTTP Communication

Synchronous communication between services:

// HTTP Client for inter-service communication
class HttpClient {
  constructor(baseURL, timeout = 5000) {
    this.baseURL = baseURL;
    this.timeout = timeout;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      timeout: this.timeout,
      headers: {
        'Content-Type': 'application/json',
        'X-Service-ID': process.env.SERVICE_ID,
        ...options.headers
      },
      ...options
    };
    
    try {
      const response = await fetch(url, config);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`HTTP request failed: ${error.message}`);
      throw error;
    }
  }
  
  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    return await this.request(url, { method: 'GET' });
  }
  
  async post(endpoint, data) {
    return await this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  async put(endpoint, data) {
    return await this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  async delete(endpoint) {
    return await this.request(endpoint, { method: 'DELETE' });
  }
}

// Service registry
class ServiceRegistry {
  constructor() {
    this.services = new Map();
  }
  
  register(name, url) {
    this.services.set(name, url);
  }
  
  getClient(serviceName) {
    const url = this.services.get(serviceName);
    if (!url) {
      throw new Error(`Service ${serviceName} not found`);
    }
    return new HttpClient(url);
  }
}

// Usage in order service
const serviceRegistry = new ServiceRegistry();
serviceRegistry.register('user-service', 'http://user-service:3001');
serviceRegistry.register('product-service', 'http://product-service:3003');

class OrderService {
  async createOrder(orderData) {
    // Validate user exists
    const userClient = serviceRegistry.getClient('user-service');
    const user = await userClient.get(`/users/${orderData.userId}`);
    
    if (!user) {
      throw new Error('User not found');
    }
    
    // Validate products
    const productClient = serviceRegistry.getClient('product-service');
    for (const item of orderData.items) {
      const product = await productClient.get(`/products/${item.productId}`);
      if (!product || product.stock < item.quantity) {
        throw new Error(`Product ${item.productId} not available`);
      }
    }
    
    // Create order
    const order = await this.orderRepository.create(orderData);
    
    return order;
  }
}

Event-Driven Communication

Asynchronous communication using message queues:

// Event Bus for message passing
class EventBus {
  constructor() {
    this.channels = new Map();
    this.subscribers = new Map();
  }
  
  async publish(channel, message) {
    const channelSubscribers = this.subscribers.get(channel) || [];
    
    for (const subscriber of channelSubscribers) {
      try {
        await subscriber.handle(message);
      } catch (error) {
        console.error(`Error in subscriber: ${error.message}`);
      }
    }
  }
  
  subscribe(channel, handler) {
    if (!this.subscribers.has(channel)) {
      this.subscribers.set(channel, []);
    }
    
    this.subscribers.get(channel).push(handler);
  }
  
  unsubscribe(channel, handler) {
    const channelSubscribers = this.subscribers.get(channel);
    if (channelSubscribers) {
      const index = channelSubscribers.indexOf(handler);
      if (index > -1) {
        channelSubscribers.splice(index, 1);
      }
    }
  }
}

// Event handlers
class OrderEventHandler {
  constructor(notificationService) {
    this.notificationService = notificationService;
  }
  
  async handle(event) {
    switch (event.type) {
      case 'order.created':
        await this.handleOrderCreated(event);
        break;
      case 'order.paid':
        await this.handleOrderPaid(event);
        break;
      case 'order.shipped':
        await this.handleOrderShipped(event);
        break;
    }
  }
  
  async handleOrderCreated(event) {
    const { orderId, userId } = event.data;
    
    // Send order confirmation email
    await this.notificationService.sendEmail({
      to: userId,
      template: 'order-confirmation',
      data: { orderId }
    });
  }
  
  async handleOrderPaid(event) {
    const { orderId, userId } = event.data;
    
    // Send payment confirmation
    await this.notificationService.sendEmail({
      to: userId,
      template: 'payment-confirmation',
      data: { orderId }
    });
  }
}

// Usage in services
const eventBus = new EventBus();

// Order service publishes events
class OrderService {
  async createOrder(orderData) {
    const order = await this.orderRepository.create(orderData);
    
    // Publish order created event
    await eventBus.publish('order.created', {
      type: 'order.created',
      data: { orderId: order.id, userId: order.userId }
    });
    
    return order;
  }
  
  async markOrderAsPaid(orderId) {
    const order = await this.orderRepository.update(orderId, { 
      status: 'paid',
      paidAt: new Date()
    });
    
    // Publish order paid event
    await eventBus.publish('order.paid', {
      type: 'order.paid',
      data: { orderId: order.id, userId: order.userId }
    });
    
    return order;
  }
}

// Notification service subscribes to events
class NotificationService {
  constructor() {
    this.eventHandler = new OrderEventHandler(this);
    
    // Subscribe to order events
    eventBus.subscribe('order.created', this.eventHandler);
    eventBus.subscribe('order.paid', this.eventHandler);
    eventBus.subscribe('order.shipped', this.eventHandler);
  }
  
  async sendEmail(options) {
    // Email sending logic
    console.log(`Sending email to ${options.to} with template ${options.template}`);
  }
}

API Gateway

Implement an API Gateway to handle routing, authentication, and load balancing:

// API Gateway implementation
const express = require('express');
const httpProxy = require('http-proxy-middleware');

class APIGateway {
  constructor() {
    this.app = express();
    this.services = new Map();
    this.setupMiddleware();
    this.setupRoutes();
    this.setupProxy();
  }
  
  setupMiddleware() {
    // Request logging
    this.app.use((req, res, next) => {
      console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
      next();
    });
    
    // Rate limiting
    const rateLimit = require('express-rate-limit');
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per windowMs
    });
    this.app.use(limiter);
    
    // Authentication middleware
    this.app.use(this.authenticate.bind(this));
    
    // Request body parsing
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: true }));
  }
  
  setupRoutes() {
    // Health check
    this.app.get('/health', (req, res) => {
      res.json({ status: 'healthy', timestamp: new Date().toISOString() });
    });
    
    // Service discovery endpoint
    this.app.get('/services', (req, res) => {
      const services = Array.from(this.services.entries()).map(([name, config]) => ({
        name,
        url: config.url,
        status: config.status
      }));
      res.json(services);
    });
  }
  
  setupProxy() {
    // Register services
    this.registerService('users', 'http://user-service:3001');
    this.registerService('orders', 'http://order-service:3002');
    this.registerService('products', 'http://product-service:3003');
    this.registerService('notifications', 'http://notification-service:3004');
    
    // Setup proxy for each service
    this.services.forEach((config, name) => {
      const proxy = httpProxy.createProxyMiddleware({
        target: config.url,
        changeOrigin: true,
        pathRewrite: {
          [`^/api/${name}`]: '/api'
        },
        onError: (err, req, res) => {
          console.error(`Proxy error for ${name}:`, err.message);
          res.status(503).json({ error: 'Service unavailable' });
        }
      });
      
      this.app.use(`/api/${name}`, proxy);
    });
  }
  
  registerService(name, url) {
    this.services.set(name, {
      url,
      status: 'unknown',
      lastCheck: null
    });
    
    // Start health checking
    this.startHealthCheck(name);
  }
  
  async startHealthCheck(serviceName) {
    const config = this.services.get(serviceName);
    
    const checkHealth = async () => {
      try {
        const response = await fetch(`${config.url}/health`);
        config.status = response.ok ? 'healthy' : 'unhealthy';
      } catch (error) {
        config.status = 'unhealthy';
      }
      
      config.lastCheck = new Date();
    };
    
    // Check health every 30 seconds
    setInterval(checkHealth, 30000);
    
    // Initial check
    checkHealth();
  }
  
  authenticate(req, res, next) {
    // Skip authentication for health check and public endpoints
    if (req.path === '/health' || req.path === '/services') {
      return next();
    }
    
    const token = req.headers.authorization?.replace('Bearer ', '');
    
    if (!token) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      req.user = decoded;
      next();
    } catch (error) {
      res.status(401).json({ error: 'Invalid token' });
    }
  }
  
  start(port = 3000) {
    this.app.listen(port, () => {
      console.log(`API Gateway running on port ${port}`);
    });
  }
}

// Start the gateway
const gateway = new APIGateway();
gateway.start();

Service Discovery

Implement service discovery for dynamic service registration:

// Service Discovery with Consul
const Consul = require('consul');

class ServiceDiscovery {
  constructor(consulHost = 'localhost', consulPort = 8500) {
    this.consul = new Consul({ host: consulHost, port: consulPort });
  }
  
  async registerService(serviceConfig) {
    const { name, id, address, port, tags = [], check = {} } = serviceConfig;
    
    const consulConfig = {
      name,
      id: id || `${name}-${port}`,
      address,
      port,
      tags,
      check: {
        http: `http://${address}:${port}/health`,
        interval: '10s',
        timeout: '3s',
        ...check
      }
    };
    
    try {
      await this.consul.agent.service.register(consulConfig);
      console.log(`Service ${name} registered successfully`);
    } catch (error) {
      console.error(`Failed to register service ${name}:`, error.message);
    }
  }
  
  async deregisterService(serviceId) {
    try {
      await this.consul.agent.service.deregister(serviceId);
      console.log(`Service ${serviceId} deregistered successfully`);
    } catch (error) {
      console.error(`Failed to deregister service ${serviceId}:`, error.message);
    }
  }
  
  async discoverService(serviceName) {
    try {
      const services = await this.consul.health.service({
        service: serviceName,
        passing: true
      });
      
      if (services.length === 0) {
        throw new Error(`No healthy instances of ${serviceName} found`);
      }
      
      // Load balancing: round-robin
      const service = services[Math.floor(Math.random() * services.length)];
      const { Service } = service;
      
      return {
        address: Service.Address,
        port: Service.Port,
        id: Service.ID
      };
    } catch (error) {
      console.error(`Failed to discover service ${serviceName}:`, error.message);
      throw error;
    }
  }
  
  async listServices() {
    try {
      const services = await this.consul.agent.service.list();
      return services;
    } catch (error) {
      console.error('Failed to list services:', error.message);
      return {};
    }
  }
}

// Usage in microservices
class Microservice {
  constructor(name, port) {
    this.name = name;
    this.port = port;
    this.serviceDiscovery = new ServiceDiscovery();
    this.serviceId = `${name}-${port}`;
  }
  
  async start() {
    // Start the service
    this.server = express().listen(this.port, () => {
      console.log(`${this.name} running on port ${this.port}`);
    });
    
    // Register with service discovery
    await this.serviceDiscovery.registerService({
      name: this.name,
      id: this.serviceId,
      address: process.env.HOSTNAME || 'localhost',
      port: this.port,
      tags: ['nodejs', 'microservice']
    });
    
    // Setup graceful shutdown
    process.on('SIGTERM', this.shutdown.bind(this));
    process.on('SIGINT', this.shutdown.bind(this));
  }
  
  async shutdown() {
    console.log(`Shutting down ${this.name}...`);
    
    // Deregister from service discovery
    await this.serviceDiscovery.deregisterService(this.serviceId);
    
    // Close server
    if (this.server) {
      this.server.close(() => {
        console.log(`${this.name} shutdown complete`);
        process.exit(0);
      });
    }
  }
}

// Start service
const userService = new Microservice('user-service', 3001);
userService.start();

Monitoring and Logging

Centralized Logging

Implement centralized logging with ELK stack:

// Structured logging with Winston
const winston = require('winston');
const Elasticsearch = require('winston-elasticsearch');

class Logger {
  constructor(serviceName) {
    this.serviceName = serviceName;
    this.logger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
      ),
      defaultMeta: { 
        service: serviceName,
        version: process.env.APP_VERSION || '1.0.0'
      },
      transports: [
        // Console output for development
        new winston.transports.Console({
          format: winston.format.simple()
        }),
        
        // Elasticsearch for centralized logging
        new Elasticsearch({
          level: 'info',
          clientOpts: {
            node: process.env.ELASTICSEARCH_URL || 'http://elasticsearch:9200'
          },
          index: `logs-${serviceName}-${new Date().toISOString().split('T')[0]}`
        })
      ]
    });
  }
  
  info(message, meta = {}) {
    this.logger.info(message, meta);
  }
  
  error(message, error = null, meta = {}) {
    this.logger.error(message, { error: error?.stack || error, ...meta });
  }
  
  warn(message, meta = {}) {
    this.logger.warn(message, meta);
  }
  
  debug(message, meta = {}) {
    this.logger.debug(message, meta);
  }
  
  // Request logging middleware
  requestLogger() {
    return (req, res, next) => {
      const start = Date.now();
      
      res.on('finish', () => {
        const duration = Date.now() - start;
        
        this.info('HTTP Request', {
          method: req.method,
          url: req.originalUrl,
          statusCode: res.statusCode,
          duration,
          userAgent: req.get('User-Agent'),
          ip: req.ip,
          userId: req.user?.id
        });
      });
      
      next();
    };
  }
}

// Usage in services
const logger = new Logger('user-service');

app.use(logger.requestLogger());

app.get('/users/:id', async (req, res) => {
  try {
    logger.info('Fetching user', { userId: req.params.id });
    
    const user = await userService.findById(req.params.id);
    
    if (!user) {
      logger.warn('User not found', { userId: req.params.id });
      return res.status(404).json({ error: 'User not found' });
    }
    
    logger.info('User fetched successfully', { userId: req.params.id });
    res.json(user);
  } catch (error) {
    logger.error('Error fetching user', error, { userId: req.params.id });
    res.status(500).json({ error: 'Internal server error' });
  }
});

Metrics Collection

Collect metrics with Prometheus:

// Prometheus metrics collection
const prometheus = require('prom-client');

class MetricsCollector {
  constructor(serviceName) {
    this.serviceName = serviceName;
    
    // Create metrics
    this.httpRequestsTotal = new prometheus.Counter({
      name: 'http_requests_total',
      help: 'Total number of HTTP requests',
      labelNames: ['method', 'route', 'status_code', 'service']
    });
    
    this.httpRequestDuration = new prometheus.Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'service'],
      buckets: [0.1, 0.5, 1, 2, 5]
    });
    
    this.activeConnections = new prometheus.Gauge({
      name: 'active_connections',
      help: 'Number of active connections',
      labelNames: ['service']
    });
    
    this.databaseConnections = new prometheus.Gauge({
      name: 'database_connections',
      help: 'Number of database connections',
      labelNames: ['service', 'type']
    });
  }
  
  // Middleware for collecting HTTP metrics
  httpMetricsMiddleware() {
    return (req, res, next) => {
      const start = Date.now();
      
      res.on('finish', () => {
        const duration = (Date.now() - start) / 1000;
        const route = req.route?.path || req.path;
        
        this.httpRequestsTotal
          .labels(req.method, route, res.statusCode.toString(), this.serviceName)
          .inc();
        
        this.httpRequestDuration
          .labels(req.method, route, this.serviceName)
          .observe(duration);
      });
      
      next();
    };
  }
  
  // Increment active connections
  incrementActiveConnections() {
    this.activeConnections.labels(this.serviceName).inc();
  }
  
  // Decrement active connections
  decrementActiveConnections() {
    this.activeConnections.labels(this.serviceName).dec();
  }
  
  // Set database connections
  setDatabaseConnections(type, count) {
    this.databaseConnections.labels(this.serviceName, type).set(count);
  }
  
  // Metrics endpoint
  getMetrics() {
    return prometheus.register.metrics();
  }
}

// Usage in services
const metrics = new MetricsCollector('user-service');

app.use(metrics.httpMetricsMiddleware());

// Track connections
app.use((req, res, next) => {
  metrics.incrementActiveConnections();
  res.on('finish', () => {
    metrics.decrementActiveConnections();
  });
  next();
});

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

// Track database connections
const pool = mysql.createPool(config);
pool.on('connection', () => {
  metrics.setDatabaseConnections('mysql', pool._allConnections.length);
});

Best Practices

Design Best Practices

  • Single Responsibility: Each service should have one clear business purpose
  • Database per Service: Avoid shared databases between services
  • Async Communication: Prefer event-driven communication over synchronous calls
  • Circuit Breakers: Implement circuit breakers to prevent cascading failures
  • Retry Mechanisms: Implement exponential backoff for failed requests

Deployment Best Practices

  • Immutable Infrastructure: Use immutable containers for deployments
  • Health Checks: Implement comprehensive health checks
  • Graceful Shutdown: Handle SIGTERM and SIGINT signals properly
  • Resource Limits: Set appropriate memory and CPU limits
  • Security Scanning: Regularly scan images for vulnerabilities

Monitoring Best Practices

  • Structured Logging: Use structured logging with consistent format
  • Distributed Tracing: Implement request tracing across services
  • Metrics Collection: Collect business and technical metrics
  • Alerting: Set up meaningful alerts for anomalies
  • Dashboards: Create comprehensive monitoring dashboards

Conclusion

Building microservices with Docker and Node.js provides excellent scalability, maintainability, and deployment flexibility. While the initial complexity is higher than monolithic applications, the long-term benefits in terms of team autonomy, independent scaling, and fault isolation make it a worthwhile investment for growing applications.

Remember that microservices are not a silver bullet. Start with a clear understanding of your business domains, implement proper communication patterns, and invest in monitoring and observability from the beginning. With proper planning and implementation, microservices architecture can help you build robust, scalable applications that can grow with your business needs.

Related Articles

Building Scalable APIs with Node.js

Learn how to architect robust REST APIs.

Read More →

Securing Your Node.js Applications

Essential security practices for Node.js apps.

Read More →

Master Microservices?

Get advanced microservices patterns and DevOps tips weekly.