In this post I share with you how I set up a simple Docker container for Node.js development. I’ve never found a simple tutorial for beginners that meets my needs!

My requirement is to do all development debugging in a container, so I am assured of consistent behaviour in any environment. I do not want to install Node.js or npm locally on my mac! I cannot be bothered to use a Makefile or Dockerfile to build images - this is only required in the final stage for production!

This is the first in a series of posts on my journey with containers and orchestration. The next post introduces you to Wiring up (networking) Node.js and MongoDB containers.

Prerequisites

You’ll need to install Docker Desktop and Microsoft Visual Studio Code 1.10+ - both are available for Windows and mac OS.

To be clear, this is simply a setup for developers - this is not the way to use containers in production (i.e. please go ahead and build images with Dockerfile). In addition, I only advocate downloading "Official" or "Docker Certified" images off Docker Hub.

Running local Node.js source code in a container

In the host (i.e. on our macOS terminal):

Create a working directory e.g. ~/Work/node and in that directory, create hello.js with:

var http = require('http');
var server = http.createServer(function(req, res) {
    res.writeHead(200);
    res.end('Hello world');
});
server.listen(8080);

Share the folder in Docker > Preferences... > File Sharing:

  • Add the folder
  • Hit Apply & Restart

Docker Preferences File Sharing

Back in the terminal, download and run a new Node.js container. The run arguments are:

  • -it is shorthand for --interactive --tty to get the console by running the given bash shell
  • --rm to delete the container when done, since we always use the original image without persisting changes
  • -v to mount a local volume (the current folder $PWD) to the given folder in the container
  • -p to “publish” the port so the host can get to it, in this case 8080
    docker pull node #download Node.js image
    docker run -it --rm -v $PWD:/code -p 8080:8080 node bash

Now the terminal will be running the shell within the container! You’ll know because the prompt will be something like root@c7dfcf365b8b:

I prefer to specify bash or /bin/bash as the shell, because while the official node image does run bash, many other images specify an ENTRYPOINT (i.e. a specific executable or comamnd to run when the container starts) and do not run the shell!

In the container terminal, run:

cd /code
ls #you’ll see your file hello.js confirming the mount
docker hello.js #to run hello.js, waiting for a connection

Open a browser to locahost:8080 and you should get the Hello world message!

To stop the program, back in the container terminal, hit Ctrl-C.

Hot reloading code changes with nodemon

In the container, install nodemon to monitor for changes to hello.js and automatically restart the node server.

npm install -g nodemon
nodemon hello.js

Now, you can make changes to hello.js and node will restart. Try it - make a change to the “hello world” output and refresh your browser.

“Saving” the new container with nodemon, etc.

Because the container above is not persistent, when you stop the container (with Ctrl-C) and re-start, the installed nodemon would be gone. So, let’s create a new image of Node.js persisting the installed nodemon.

Run the container as above but give it a name, nodedev:

docker run -it --name nodedev -v $PWD:/code -p 8080:8080 node bash

In the container, install nodemon and exit (which stops the container):

npm install -g nodemon
exit

At this stage you should install any other Node.js modules you need in the container, once and for all!

Now, make the change persistent in a new image called nodemondev, and then run the container again to test (note I don’t run with --name but instead use --rm again because I’m not going to persist subsequent changes):

docker commit nodedev nodemondev
docker run -it --rm -v $PWD:/code -p 8080:8080 nodemondev bash

In the container, you can type nodemon and viola! nodemon is already installed!

In addition, if you want to just immediately run nodemon but yet have the console respond to Ctrl-C to stop node (rather than have to manually stop the container), just run:

docker run -it --rm -v $PWD:/code -p 8080:8080 nodemondev bash -c "cd /code && nodemon hello.js"

Debugging via Visual Studio Code

VS Code is great for many reasons - one of which is that it can attach to a running Node.js instance and debug the code with step-through and breakpoints.

The important settings when starting Node.js (which in this case I do via nodemon in a container) are:

  • turn on debugging with --inspect=0.0.0.0:9229, where 0.0.0.0 would allow all ingress (remote connections), otherwise debugging is only available from within the container.
  • and, publish the debugging port 9229 to the host with -p 127.0.0.1:9229:9229.
docker run -it --rm -v $PWD:/code -p 8080:8080 -p 127.0.0.1:9229:9229 nodemondev bash -c "cd /code && nodemon --inspect=0.0.0.0:9229 hello.js"

Note that the Node.js Debugging guide states "Exposing the debug port publicly is unsafe", so we mitigate by only exposing the port to only the host hence the explicit 127.0.0.1. However, port 8080 is public - but now you know how to change it if so desired.

Now, the magic from VS Code:

  • Hit the debugging icon (or from the menu View > Debug)
  • Next to the green play button, hit the dropdown and select Add Configuration....
  • In the new editor launch.json, select Attach to Process.
  • The block below will be added and you must add the remoteroot path manually (remember the comma in the preceding line). Save and close:
{
  "type": "node",
    "request": "attach",
    "name": "Attach by Process ID",
    "processId": "${command:PickProcess}",
    "remoteRoot": "/code"
}

Without remoteRoot, you’ll get an Unverified breakpoint message whenever you try to set a breakpoint!

Now hit the green play button and you’ll be prompted to attach to a Docker container. Simply select yours:

Visual Studio Code Attach to Node.js process

To test, place a breakpoint at res.writeHead(200); by pressing F9, and refresh the page in your browser. You’ll suddenly be back in VS Code with variables, call stack, etc. displayed. Brilliant!

Visual Studio Code Debug Node.js code in a container

BTW, if you are too lazy to choose a process above, you could pre-configure everything. The downside is that every new container will need a section like this, where as with the above, as you can select from many containers with different debugging ports (as long as code is always in the /code folder).

{
    "type": "node",
    "request": "attach",
    "name": "Attach to development Node container",
    "address": "localhost",
    "port": 9229,
    "remoteRoot": "/code",
    "restart": true
}

You can read more about the options at Node.js debugging in VS Code.

Running the docker container from VS Code

Still want more? How about running the docker command line from the green play button in VS Code directly?

Create another debug configuration in the launch.json file as below:

{
    "type": "node",
    "request": "launch",
    "name": "Launch development Node container",
    "address": "localhost",
    "port": 9229,
    "remoteRoot": "/code",
    "restart": true,
    "console": "integratedTerminal",
    "runtimeExecutable": "docker",
    "runtimeArgs": [
        "run","-it","--rm","-v","${workspaceFolder}:/code",
        "-p","8080:8080","-p","127.0.0.1:9229:9229","nodemondev","bash",
        "-c","cd /code && nodemon --inspect=0.0.0.0:9229 hello.js"
    ]
}

Now you never have to leave VS Code, even the Terminal is embedded!

Conclusion

It’s really easy to use Docker containers for development, just a little bit of overhead which you can easily script. This provides a more consistent environment without having to install Node.js, etc. on your mac or Windows desktop, more so if your servers are mostly Linux-based.

On to the next post, Wiring up Node.js and MongoDB containers!