Phase 9 - Deployment | Post 13 | Docker and Docker Compose — Running Your Entire Application With One Command

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

Phase 9 - Final Post | Post 15 | What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead

Post 15 of 15 | Final Post What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead You have completed the core Mo...