Zac Fukuda
054

Docker Compose Node.js+MongoDB in 2024

docker-compose.yml
version: '3.8'
services:
  mongo:
    image: mongo:6.0.13
    environment:
      MONGO_INITDB_DATABASE: ${MONGO_DATABASE}
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
    volumes:
      - mongo-config:/data/configdb
      - mongo-data:/data/db
  node:
    image: node:20.11.0-alpine
    user: ${NODE_USER}
    working_dir: /home/node/app
    environment:
      - NODE_ENV=${NODE_ENV}
    command: "yarn start"
    ports:
      - 3000:${NODE_PORT}
    volumes:
      - ./:/home/node/app
    depends_on:
      - mongo
volumes:
  mongo-config:
  mongo-data:

This docker compose is designed of local development only.

Alpine

This Docker compose is based on Alpine Linux project.

While the node:20.11.0 image amounts to 1.09 gb, the node:20.11.0-alpine only amounts to 135.93 mb. I think we are better off with alpine version as long as we don’t encounter any issue, only if you use the same image for production.

Volumes

Docker create a new volume every time you run docker compose up or docker compose start for /data/configdb. So I believe it is good practice to specify mongo-config:/data/configdb to the volumes of MongoDB service.

If you would not like to have two volumes, you can have single volume mount to /data.

Networks

You may want to specify networks.

version: '3.8'
services:
  mongo:
    …
    networks:
      - nodejs-mongodb
  node:
    …
    networks:
      - nodejs-mongodb
networks:
  nodejs-mongodb:
    driver: bridge

Node Command

I prefer yarn start.

I don’t know why but yarn start command stops container much faster than npm run start.

With npm run start it takes roughly 10s to stop the container. With yarn start it takes less than 1s.

Two commands do not make a difference when create or start a container.

Environment

Replace ${…} with string if you don’t want to rely on .env.

Create Node App

The rest of article is about creating an example application to see if this docker compose actually works in more realistic development environment.

You need three additional files:

.
├── .env
├── package.json
└── server.js

Run the next command to create them:

touch .env package.json server.js

Add the following codes to each file:

.env
# Docker Node.js
NODE_USER=node
NODE_ENV=production
NODE_PORT=3000

# Docker MongoDB
MONGO_HOST=mongo
MONGO_PORT=27017
MONGO_DATABASE=example
MONGO_USERNAME=root
MONGO_PASSWORD=example
package.json
{
  "scripts": {
    "start": "nodemon -L server.js",
  },
  "dependencies": {
    "body-parser": "^1.20.2",
    "dotenv": "^16.4.1",
    "express": "^4.18.2",
    "mongodb": "^6.3.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.3"
  }
}

You need to pass -L flag when you run Nodemon via container. See Application isn't restarting.

server.js
require('dotenv').config()

const express = require('express')
const bodyParser = require("body-parser")
const { MongoClient } = require('mongodb')
const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOST, MONGO_PORT, MONGO_DATABASE, NODE_PORT } = process.env
const app = express()
const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOST}:${MONGO_PORT}`
const client = new MongoClient(url)
const collectionName = 'books'

app.use(bodyParser.json())
app.get('/', function (req, res) {
  res.send('Hello, world')
})
app.get('/books', async function(req, res) {
  await client.connect()
  const db = client.db(MONGO_DATABASE)
  const collection = db.collection(collectionName)
  const books = await collection.find({}).toArray()

  client.close()
  res.json({ data: { books } })
})
app.post('/books', async function(req, res) {
  await client.connect()
  const db = client.db(MONGO_DATABASE)
  const collection = db.collection(collectionName)
  const result = await collection.insertOne(req.body)

  client.close()
  res.json({ data: { result } })
})
app.listen(NODE_PORT)

MONGO_INITDB_ROOT_USERNAME defined in a compose file will be added to the authentication database admin. Users of this database cannot access to specific database directly. The urls that can/cannot connect MongoDB are listed below.

// Work
const url = "mongodb://root:example@mongo:27017"
const url = "mongodb://root:example@mongo:27017/example?authSource=admin" // do "client.db()" without an argument

// Not work
const url = "mongodb://root:example@mongo:27017/example"
const url = "mongodb://mongo:mongo/example"

Install Node Modules

npm install
# or
yarn

I am not sure if pnpm would work with Docker’s volume mounting.

Compose

# Up
docker compose up --build -d

# Start/stop containers
docker compose start
docker compose stop

# Stop and remove containers, networks
docker compose down

Open http://localhost:3000. You see “Hello, world” message.

Next, open http://localhost:3000/books. This time you see JSON string like this:

{"data":{"books":[]}}

Now open your terminal and make a couple of curl requests:

curl -X POST -H "Content-Type:application/json" -d '{"title": "Atomic Habits"}' http://localhost:3000/books
curl -X POST -H "Content-Type:application/json" -d '{"title": "The Psychology of Money"}' http://localhost:3000/books

You will get a response(output) like below for each request:

{"data":{"result":{"acknowledged":true,"insertedId":"65bb3…"}}}

Back to http://localhost:3000/books, you get an array of two books:

{"data":{"books":[{"_id":"65bb3…","title":"Atomic Habits"},{"_id":"65bb3…","title":"The Psychology of Money"}]}}

Logs

When you run a container in daemon, you cannot see the log of Node.js process when any error or bug occurs. You have two options to check the log.

1. Docker Dashboard

  1. Open Docker Dashboard
  2. Click “Containers”
  3. Click your node container
  4. You can see the logs

2. VS Code Docker extension

  1. Install “Docker” extension if not installed, then restart VS Code
  2. Open “Command Palette” (Shift + Cmd + p)
  3. Select “Docker Containers: View Logs”
  4. Choose your stack
  5. Choose your node container
  6. VS Code shows the container logs in Terminal

Resources