Post 13 of 15 | Phase 9: Deployment
Docker and Docker Compose — Running Your Entire Application With One Command
In every post so far you have been running everything on your local machine manually. MongoDB running separately, Node.js process running with npm run dev, maybe NATS or Redis running in another terminal. In production and even in team development, this manual setup is not sustainable.
Docker solves this by packaging each service into a self-contained unit called a container. Docker Compose then lets you define your entire application — all services, databases, message brokers — in one file and start everything with a single command.
By the end of this post you will be able to run your entire microservices application with:
docker-compose up
And tear it all down with:
docker-compose down
What is Docker — The Core Idea
Think of Docker as a shipping container for software. Before shipping containers existed, loading cargo onto a ship was chaos. Every item had different shapes, sizes, and handling requirements. Shipping containers standardized everything. Any container fits on any ship, any truck, any crane.
Docker does the same for software. Instead of worrying about what operating system the server runs, what version of Node.js is installed, what environment variables are set — you package your application and everything it needs into a container image. That image runs the same way on your laptop, your teammate's laptop, and your production server.
Key Docker Concepts
Dockerfile — A text file with instructions for building a container image. Like a recipe.
Image — The built result of a Dockerfile. A blueprint. Not running yet.
Container — A running instance of an image. The actual running application.
Docker Compose — A tool for defining and running multiple containers together. Uses a docker-compose.yml file.
Volume — A way to persist data outside the container. Without volumes, data is lost when a container stops.
Network — Containers on the same Docker network can communicate with each other by service name.
Installing Docker
Go to docker.com/get-started and download Docker Desktop for Windows. Install it and start it. Verify installation:
docker --version
docker-compose --version
You should see version numbers for both.
Writing the Dockerfile for Your Services
Every Node.js service needs a Dockerfile. Since all your services are in one project, you write one Dockerfile at the project root that works for all of them.
Create Dockerfile in your project root:
# Use the official Node.js 18 image as the base
# alpine is a minimal Linux distribution — smaller image size
FROM node:18-alpine
# Set the working directory inside the container
# All subsequent commands run from this directory
WORKDIR /app
# Copy package.json and package-lock.json first
# Docker caches layers — copying package files separately means
# npm install only reruns when dependencies change, not on every code change
COPY package*.json ./
# Install dependencies
# --production skips devDependencies
RUN npm install --production
# Copy the rest of your application code
COPY . .
# Expose the port your API Gateway listens on
# This is documentation — it does not actually publish the port
EXPOSE 3000
# Default command to run when container starts
# Can be overridden in docker-compose.yml per service
CMD ["node", "-r", "dotenv/config", "node_modules/.bin/moleculer-runner", "services"]
Create a .dockerignore file to prevent unnecessary files from being copied into the image:
node_modules
.env
logs
*.log
.git
.gitignore
README.md
This is like .gitignore but for Docker. It makes builds faster and images smaller.
Understanding the Application Architecture with Docker
Before writing docker-compose.yml, understand what containers you need:
Containers:
api — API Gateway service (port 3000 exposed to outside)
user — User service (internal only)
order — Order service (internal only)
product — Product service (internal only)
email — Email service (internal only)
notification — Notification service (internal only)
mongo — MongoDB database
redis — Redis for caching and transporter
jaeger — Jaeger for distributed tracing (optional)
Network:
All containers on the same Docker network
They communicate using container names as hostnames
Volumes:
mongo-data — MongoDB data persists even when container restarts
redis-data — Redis data persists
In this setup, each Moleculer service runs in its own container. They all connect through Redis as the transporter. This is the correct production microservices architecture.
Writing docker-compose.yml
Create docker-compose.yml in your project root:
version: "3.8"
# Named volumes — data persists between container restarts
volumes:
mongo-data:
redis-data:
# All containers on this network can reach each other by service name
networks:
moleculer-net:
driver: bridge
services:
# MongoDB database
mongo:
image: mongo:6
container_name: mongo
restart: unless-stopped
environment:
MONGO_INITDB_DATABASE: moleculer-course
volumes:
# Persist MongoDB data on your machine
- mongo-data:/data/db
networks:
- moleculer-net
# Not exposed to outside — only accessible within Docker network
ports:
- "27017:27017" # Expose for local MongoDB Compass access
# Redis — used as both cacher and transporter
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- moleculer-net
ports:
- "6379:6379" # Expose for local Redis inspection
# Jaeger — distributed tracing UI
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
restart: unless-stopped
networks:
- moleculer-net
ports:
- "6831:6831/udp" # Jaeger agent port for traces
- "16686:16686" # Jaeger UI — open in browser
# API Gateway — the only service exposed to the outside world
api:
build:
context: .
dockerfile: Dockerfile
container_name: api
restart: unless-stopped
environment:
# Service-specific environment variables
SERVICES: api # Only load api.service.js
PORT: 3000
NODE_ENV: production
MONGO_URI: mongodb://mongo:27017/moleculer-course
REDIS_URI: redis://redis:6379
JWT_SECRET: your-secret-change-this-in-production
JAEGER_HOST: jaeger
depends_on:
- mongo
- redis
- jaeger
networks:
- moleculer-net
ports:
- "3000:3000" # Only this service is exposed to the internet
labels:
- "service=api-gateway"
# User service
user:
build:
context: .
dockerfile: Dockerfile
container_name: user
restart: unless-stopped
environment:
SERVICES: user # Only load user.service.js
NODE_ENV: production
MONGO_URI: mongodb://mongo:27017/moleculer-course
REDIS_URI: redis://redis:6379
JWT_SECRET: your-secret-change-this-in-production
JAEGER_HOST: jaeger
depends_on:
- mongo
- redis
networks:
- moleculer-net
# Product service
product:
build:
context: .
dockerfile: Dockerfile
container_name: product
restart: unless-stopped
environment:
SERVICES: product
NODE_ENV: production
MONGO_URI: mongodb://mongo:27017/moleculer-course
REDIS_URI: redis://redis:6379
JAEGER_HOST: jaeger
depends_on:
- mongo
- redis
networks:
- moleculer-net
# Order service
order:
build:
context: .
dockerfile: Dockerfile
container_name: order
restart: unless-stopped
environment:
SERVICES: order
NODE_ENV: production
MONGO_URI: mongodb://mongo:27017/moleculer-course
REDIS_URI: redis://redis:6379
JAEGER_HOST: jaeger
depends_on:
- mongo
- redis
networks:
- moleculer-net
# Email service
email:
build:
context: .
dockerfile: Dockerfile
container_name: email
restart: unless-stopped
environment:
SERVICES: email
NODE_ENV: production
REDIS_URI: redis://redis:6379
depends_on:
- redis
networks:
- moleculer-net
# Notification service
notification:
build:
context: .
dockerfile: Dockerfile
container_name: notification
restart: unless-stopped
environment:
SERVICES: notification
NODE_ENV: production
REDIS_URI: redis://redis:6379
depends_on:
- redis
networks:
- moleculer-net
The SERVICES Environment Variable — How One Dockerfile Runs Different Services
Notice that every service container uses the same Dockerfile but a different SERVICES environment variable. This is the key pattern.
The moleculer-runner command reads the SERVICES environment variable to decide which service files to load. So:
SERVICES: api → loads only services/api.service.js
SERVICES: user → loads only services/user.service.js
SERVICES: order → loads only services/order.service.js
One Docker image, many containers, each running a different service. This is clean and efficient.
Updating moleculer.config.js for Docker
Your config needs to read from environment variables so it works both locally and in Docker:
"use strict";
require("dotenv").config();
module.exports = {
namespace: "ecommerce",
nodeID: null,
logger: {
type: "Console",
options: {
formatter: process.env.NODE_ENV === "production" ? "json" : "full",
colors: process.env.NODE_ENV !== "production",
autoPadding: true
}
},
logLevel: process.env.LOG_LEVEL || "info",
// Redis as transporter — connects all service containers
// When REDIS_URI is not set, fall back to null (single process mode)
transporter: process.env.REDIS_URI
? `redis://${process.env.REDIS_URI.replace("redis://", "")}`
: null,
// Redis as cacher
cacher: process.env.REDIS_URI
? {
type: "Redis",
options: {
redis: process.env.REDIS_URI,
ttl: 30
}
}
: {
type: "Memory",
options: { ttl: 30 }
},
// Metrics
metrics: {
enabled: process.env.NODE_ENV === "production",
reporter: [
{
type: "Prometheus",
options: {
port: 3030,
path: "/metrics"
}
}
]
},
// Tracing
tracing: {
enabled: true,
exporter: process.env.JAEGER_HOST
? [
{
type: "Jaeger",
options: {
host: process.env.JAEGER_HOST,
port: 6832,
sampler: {
type: "Const",
options: { decision: 1 }
}
}
}
]
: [
{
type: "Console",
options: { colors: true, width: 100, gaugeWidth: 40 }
}
]
},
// Fault tolerance
requestTimeout: 10 * 1000,
retryPolicy: {
enabled: true,
retries: 3,
delay: 100,
maxDelay: 2000,
factor: 2,
check: err => err && !!err.retryable
},
circuitBreaker: {
enabled: true,
threshold: 0.5,
minRequestCount: 20,
windowTime: 60,
halfOpenTime: 10 * 1000,
check: err => err && err.code >= 500
},
bulkhead: {
enabled: true,
concurrency: 10,
maxQueueSize: 100
}
};
Also update your service files to read MONGO_URI from environment:
// In every service that uses MongoDB
adapter: new MongooseAdapter(
process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
),
Running the Application
Build all images and start all containers:
docker-compose up --build
The first run takes a few minutes as Docker downloads base images and builds your application images. Subsequent runs are much faster because Docker caches layers.
You will see logs from all containers interleaved in the terminal. Each line is prefixed with the container name so you can tell which service produced it.
To run in the background:
docker-compose up --build -d
Check all containers are running:
docker-compose ps
You should see all containers with status Up.
Useful Docker Compose Commands
docker-compose up --build Start everything and rebuild images
docker-compose up -d Start in background (detached mode)
docker-compose down Stop and remove all containers
docker-compose down -v Stop containers and delete volumes (wipes data)
docker-compose ps List running containers and their status
docker-compose logs Show logs from all containers
docker-compose logs user Show logs from only the user container
docker-compose logs -f user Follow logs in real time from user container
docker-compose restart user Restart only the user container
docker-compose exec user sh Open a shell inside the user container
docker-compose build Rebuild images without starting
docker-compose stop Stop containers without removing them
docker-compose start Start previously stopped containers
Scaling a Service
One of the best features of this architecture is scaling. If your product service is getting too many requests, run three instances with one command:
docker-compose up --scale product=3 -d
Now three product containers are running. They all connect to Redis as the transporter. Moleculer automatically distributes requests across all three using RoundRobin load balancing.
No code changes. No configuration changes. Just one command.
Checking That Everything Works
After docker-compose up, test your endpoints:
Register a user:
POST http://localhost:3000/api/public/register
Body: {
"name": "Rahul Sharma",
"email": "rahul@example.com",
"password": "password123"
}
Login:
POST http://localhost:3000/api/public/login
Body: {
"email": "rahul@example.com",
"password": "password123"
}
Create a product:
POST http://localhost:3000/api/product
Body: {
"name": "Mechanical Keyboard",
"price": 2999,
"stock": 50
}
Place an order (with JWT token in Authorization header):
POST http://localhost:3000/api/order
Headers: Authorization: Bearer <token>
Body: {
"productId": "<id from above>",
"quantity": 1
}
Open Jaeger at http://localhost:16686 and search for traces. Select the ecommerce service from the dropdown and click Find Traces. You will see the full request chain visualized as a timeline.
Complete Final Folder Structure
Your project at the end of this post:
my-project/
models/
user.model.js
product.model.js
order.model.js
services/
api.service.js
user.service.js
product.service.js
order.service.js
email.service.js
notification.service.js
logs/
(generated at runtime)
Dockerfile
docker-compose.yml
.dockerignore
moleculer.config.js
package.json
.env
.gitignore
Common Issues and Fixes
Issue: Container exits immediately
Check logs:
docker-compose logs <service-name>
Usually a missing environment variable or connection refused to MongoDB or Redis.
Issue: Services cannot connect to MongoDB
Make sure MONGO_URI uses the container name as hostname not localhost:
# Wrong inside Docker
MONGO_URI: mongodb://localhost:27017/moleculer-course
# Correct inside Docker — use container name
MONGO_URI: mongodb://mongo:27017/moleculer-course
Issue: npm install fails during build
Make sure .dockerignore excludes node_modules so Docker does a clean install inside the container.
Issue: Changes to code not reflected
Rebuild the image:
docker-compose up --build
Summary
- Docker packages your application into portable containers that run the same everywhere.
- Dockerfile defines how to build your application image. One Dockerfile works for all services.
- The SERVICES environment variable tells moleculer-runner which service to load in each container.
- docker-compose.yml defines all containers, their environment variables, networks, and volumes.
- All containers on the same Docker network communicate using container names as hostnames.
- Use mongo and redis as hostnames inside Docker, not localhost.
- Redis serves as both the transporter and cacher in the containerized setup.
- Volumes persist data between container restarts. Without them data is lost.
- docker-compose up --build starts everything. docker-compose down stops everything.
- Scaling a service is one command: docker-compose up --scale product=3.
- Jaeger runs as a container and is accessible at localhost:16686.
Up Next
Post 14 is the final post — the complete e-commerce project review, best practices, what to learn next, and how to take your Moleculer knowledge to production. We tie together everything from all 13 previous posts into a complete picture.
Course Progress: 13 of 15 posts complete.
No comments:
Post a Comment