Building Scalable Microservices with Node.js and Docker
A deep-dive into decomposing monoliths into services, containerising them with Docker, and wiring everything together with an API gateway and service discovery.
What Are Microservices?
Microservices is an architectural style that structures an application as a collection of small, independently deployable services. Each service owns its data, communicates over well-defined APIs, and can be scaled, deployed, and restarted without affecting the rest of the system.
Creating Your First Service
Start with a minimal Express app that serves a single responsibility. Keep it stateless, expose a health-check endpoint, and drive all configuration through environment variables.
const express = require('express');
const app = express();
app.use(express.json());
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.listen(process.env.PORT ?? 3001, () =>
console.log('user-service ready'),
);Containerising with Docker
Use multi-stage builds so the final image contains only the production artifact — no dev dependencies, no build tooling.
# ---- build ----
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- run ----
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
EXPOSE 3001
CMD ["node", "dist/server.js"]Service Communication Patterns
- Synchronous REST — simple but creates runtime coupling between services
- Async messaging via RabbitMQ / Kafka — decouples producers and consumers
- gRPC — low-latency binary protocol ideal for internal service calls
- GraphQL Federation — compose multiple service graphs into one public API
API Gateway
An API gateway is the single entry point for all clients. It handles routing, rate limiting, authentication, and request aggregation so each service doesn't need to reimplement these cross-cutting concerns.
“Design each service to do one thing and do it well. When it starts doing two things, split it.”