How to Dockerize a Node.js/Mongo App with Live Reload (nodemon)


I’ve been working on a Node.js application that interacts with a local Mongo instance. I needed a way to live reload my application on code changes.

Usually, nodemon does this very easily for us. We could install nodemon in devDependencies and then just run nodemon index.js.

However, inside a Docker container, it’s a little different.

Suppose this is my project structure:

📂project
 ┣ 📜docker-compose.yml
 ┣ 📜Dockerfile
 ┣ 📜package.json
 ┗ 📂server
    ┣ 📜index.js
    ┣ 📂views
    ┗ 📜every other file in my project...

Let’s go through each file.

Our Dockerfile will build and start just our Node.js application. This one is fairly straightforward. I commented the lines if you’re unfamiliar with how a Dockerfile works.

# Dockerfile
# Pull official Node.js image from Docker Hub
FROM node:12
# Create app directory
WORKDIR /usr/src/app
# Install dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# Expose container port 3000
EXPOSE 3000
# Run "start" script in package.json
CMD ["npm", "start"]

This docker-compose.yml allows us to communicate between containers.

The app service contains our Node.js application.

The build entry is calling our Dockerfile from above and creating a custom image for us.

We are also mounting our local source code using volumes. In this case, we are stating that the container’s folder /usr/src/app/server should simply reference my local copy of ./server. In other words, the host machine’s ./server will override the container’s copy of that entire folder.

The ports entry is telling us to map our http://localhost:8080 to the container’s exposed port 3000. This means that we can access our application through port 8080 (not 3000).

We are also using the mongo image as seen below.

# docker-compose.yml
version: "3"
services:
  app:
    container_name: node-app
    build: .
    volumes:
      - ./server:/usr/src/app/server
    command: npm start
    restart: always
    ports:
      - "8080:3000"
    external_links:
      - mongo
  mongo:
    container_name: mongo
    image: mongo
    ports:
      - "27017:27017"

The main thing to notice in our package.json is that we are running the -L flag when running nodemon, which uses legacyWatch (Chokidar polling).

Since we mounted ./server, nodemon will be able to see code changes on our local machine, and then update the Node.js application in the container.

{
  "name": "my-package",
  "version": "1.0.0",
  "main": "server/index.js",
  "scripts": {
    "start": "nodemon -L server/index.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "mongoose": "^5.10.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}

In index.js, we’ll initialize an express app.

If we’re using express and we have our own views, then we may need to specify the location of our views folder. The default location is /usr/src/app/views inside the container, but we have our views folder inside /usr/src/app/server/views.

We’ll also make a connection to MongoDB using mongoose.

// index.js
const express = require("express");
const mongoose = require("mongoose");
const app = express();
// Specify location of views
app.set("views", "./server/views");
// Connect to MongoDB
mongoose
  .connect("mongodb://mongo:27017/node-app", { useNewUrlParser: true })
  .then(() => console.log("MongoDB Connected"))
  .catch((err) => console.log(err));
// Start up the server on port 3000
const port = 3000;
app.listen(port, () => console.log(`Server running on http://localhost:8080`));