Building Scalable Microservices with Node.js and Docker
Back to Blog
October 15, 2025
11 min read

Building Scalable Microservices with Node.js and Docker

I broke my monolith into microservices and immediately regretted it. Here's what I learned about when microservices actually make sense (and when they don't).

Node.jsDockerMicroservicesArchitecture

Building Scalable Microservices with Node.js and Docker

I split my monolith into microservices because that's what you're supposed to do, right? Everyone's doing microservices.

Worst decision I made that year.

My deployment got more complex. Debugging became a nightmare. And I was still the only developer, so the "multiple teams" benefit didn't apply.

Two months later, I merged most of it back into a monolith. But I learned a lot about when microservices actually make sense.

When Microservices Actually Help

Here's the truth: microservices solve organizational problems, not technical ones.

They make sense when:

  • You have multiple teams working on the same product
  • Different parts of your app have wildly different scaling needs
  • You need to deploy parts independently

For a solo developer or small team? Probably not worth it.

But if you're going to do it anyway (or you're working somewhere that already has them), here's what actually matters.

The One Rule That Matters

Each service should do one thing. That's it.

// Good: User service handles users class UserService { async createUser(userData) { // User creation logic } async getUserById(id) { // User retrieval logic } } // Bad: User service doing payments class UserService { async createUser(userData) { } async processPayment(paymentData) { } // Wrong! }

I broke this rule early on. Had a "user service" that also handled subscriptions and billing. Debugging was hell because I never knew which service was causing issues.

The API Gateway (Actually Useful)

This is one pattern that's worth it even for small projects:

const express = require('express') const { createProxyMiddleware } = require('http-proxy-middleware') const app = express() // Route to different services app.use('/api/users', createProxyMiddleware({ target: 'http://localhost:3001', changeOrigin: true })) app.use('/api/orders', createProxyMiddleware({ target: 'http://localhost:3002', changeOrigin: true })) app.listen(3000)

One entry point for everything. Makes deployment and SSL way easier.

Docker: Keep It Simple

Here's my production Dockerfile. Nothing fancy:

FROM node:18-alpine WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy app COPY . . # Run as non-root USER node EXPOSE 3000 CMD ["node", "server.js"]

That's it. Don't over-complicate it.

Database Per Service (Maybe)

Everyone says "each service needs its own database." In theory, great. In practice, expensive and complex.

For my projects, I use one database with different schemas:

-- users schema CREATE SCHEMA users; CREATE TABLE users.accounts (...); -- orders schema CREATE SCHEMA orders; CREATE TABLE orders.purchases (...);

Services can't access each other's schemas. Good enough isolation without managing multiple databases.

A client I worked with had separate databases per service. Their AWS RDS bill was $800/month. After consolidating to one database with schemas, it dropped to $200/month.

When I Merged Back to a Monolith

After two months of microservices, I had:

  • 5 services
  • 5 Docker containers to manage
  • 5 deployment pipelines
  • 5 sets of logs to check when something broke

And I was still the only developer.

I merged 4 of them back into one app. Kept the API gateway pattern for routing, but everything runs in one process now.

Deployment got simpler. Debugging got easier. My AWS bill dropped by $40/month.

The one service I kept separate? Image processing. It's CPU-intensive and needs to scale independently. That actually made sense to split out.

What Actually Matters

If you're going to do microservices:

Start with a monolith: Seriously. Don't start with microservices. You don't know your boundaries yet.

Split only when you have to: When one part of your app needs to scale differently, or when it's causing deployment issues, then split it.

Keep services big enough: Don't make a service for every single feature. That's too many services.

Use Docker Compose for local dev: Makes running multiple services locally way easier.

# docker-compose.yml version: '3' services: api: build: ./api ports: - "3000:3000" worker: build: ./worker environment: - QUEUE_URL=redis://redis:6379

The Monitoring Problem

With a monolith, when something breaks, you check one log file. With microservices, you check 5+ log files.

I use a simple centralized logging setup:

// logger.js const winston = require('winston') const logger = winston.createLogger({ format: winston.format.json(), defaultMeta: { service: process.env.SERVICE_NAME }, transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'app.log' }) ] }) module.exports = logger

All services log to the same format. Makes debugging way easier.

Should You Do Microservices?

Honestly? Probably not.

If you're a solo developer or small team, stick with a monolith. It's simpler, cheaper, and easier to debug.

If you're at a company with multiple teams, or you have parts of your app that need to scale independently, then maybe.

But don't do microservices because it's trendy. Do it because you have a specific problem that microservices solve.

I learned this the hard way. Hopefully you won't have to.


Have you tried microservices? I'd love to hear if you kept them or went back to a monolith. Hit me up on LinkedIn.