Using Docker for Nodejs development can be a pleasant experience! At least it is for me, using the setup described in this article. There are some issues I initially was bothered by, which I am solving in this post:

  • How can I automatically restart my nodejs application inside a Docker container on code change?
  • How can I debug my Dockerized Nodejs application?
  • How can I install new packages in the docker container?

Photo by Cameron Venti

Prerequisites:

You should have the following software installed if you wish to use this setup:

  • Docker
  • Docker compose
  • Nodejs

Your node app

The node app in this example is a simple expressjs hello-world server.

src/index.js

const express = require('express')
const app = express()
const port = 4000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

The application is currently only using express as a dependency in the package.json file.

Create the Dockerfile

I prefer calling my Dockerfile for development dev.Dockerfile to separate it from the file used for production containers.

dev.Dockerfile

FROM node:alpine

WORKDIR /usr/src/app

RUN npm install pm2 -g
COPY package.json package-lock.json ./
RUN npm install

COPY . ./

CMD [ "pm2-runtime", "start", "./ecosystem.config.js" ]

I like using the Alpine version to avoid large image sizes, it’s only ~5MB compared to Ubuntu’s ~190MB. Note that we are installing the package pm2 globally in our image. Pm2 is a process manager that allows us to watch our code directory and automatically restart the application when code changes

Use .dockerignore to prevent the copy of node_modules to the image

Our Dockerfile is copying everything in the project root to the image (The command COPY . ./). We don’t want it to include the node_modules directory. Because it can contain code built for our host operating system specfically - we want our image to it’s own copy of node_modules To prevent this we create a .dockerignore file. Not much different from a .gitignore

.dockerignore

node_modules

Create docker-compose.yaml

Our docker-compose.yaml is using our new Dockerfile, dev.Dockerfile Note that we are mounting the directory src to the container using bind mount. This enables us to access our code in the docker container, while editing it on the host machine: the moment we save a file the changes are available in the container.

docker-compose.yaml

version: "3"
services:
  my-nodejs-server:
    build:
      context: ./
      dockerfile: ./dev.Dockerfile
    ports:
    - "4000:4000"
    - "9229:9229"
    volumes:
    - ./src/:/usr/src/app/src/

We are exposing port 4000 to be able to access the application from our host machine. We are also exposing port 9229 to be able to debug our Dockerized nodejs application

Create an ecosystem.config file for PM2

The final piece of the puzzle is to create an ecosystem.config file that pm2 can use to run our application in development.

ecosystem.config.js

module.exports = {
  apps: [{
    name: 'my-nodejs-server',
    script: './src/index.js',
    max_memory_restart: '1000M',
    watch: ["src"],
    node_args: ["--inspect=0.0.0.0:9229"],
    watch_delay: 1000,
    ignore_watch : ["node_modules"]
  }],
};

By having the property watch: ['src'] we are telling pm2 to restart the process whenever a file changes within the directory “src”. I have seen that bind mounts and pm2 watching can be problematic on Windows. To solve this you should run everything within WSL2 (Windows subsystem for Linux 2). I recommend using WSL2 for all your Nodejs development on Windows.

Start developing

To run your development container and start developing, you simply run docker-compose up. Now your application inside the Docker container will automatically restart once you edit your code in your favorite code editor.

Installing new packages

When you need to install new packages, you do the normal npm install x, which adds the package to package.json. You also need to run docker-compose build to rebuild your image to install the new package in the development container as well.

Debugging our Dockerized Nodejs application from VS code

Create this file:

./.vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Docker: Attach to Node",
            "type": "node",
            "request": "attach",
            "port": 9229,
            "address": "0.0.0.0",
            "localRoot": "${workspaceFolder}/server/",
            "remoteRoot": "/usr/src/app",
            "protocol": "inspector",
            "skipFiles": [
                "<node_internals>/**"
            ],
        }
    ]
}

Now you can simply press F5 in VS code and attach a debugger.

Full example

A full example of this is available in this GitHub repository.

Conclusion

This provides an easy to use setup for developing with Nodejs inside a docker container, without losing the ability to debug or having to manually restart the process after every code change.