The built-in debugger of VSCode is super handy for debugging nodejs applications. Sure, there are other debuggers available, but if you are using VSCode for building your Typescript or ES6 applications, it's very convenient.

Getting the VSCode debugger working with a nodejs application is very simple. However, we like to make things complex, haha! If you're building your application using Typescript or ES6, that requires more configuration. If you're using Docker to run your nodejs application for development, that also requires more configuration. It would also be nice if node and the debugger would restart automatically after we make changes to our source code and if we could do all of this with just 1 click of a button. That also takes even more configuration to do.

Luckily, the VSCode debugger is very flexible and powerful. It can handle all of our wishes above, and more. However, it takes a bit of work to get it working. That's why you're reading this post right now. Let's get the VSCode debugger working on a Typescript oe ES6 application running inside of a Docker container, restarting automatically on code change, and starting up all with 1 click of a button.

Note: The instructions below include specific instructions for using Typescript but the instructions also work for ES6 applications. If you are writing your application using ES6 and not Typescript, follow the guides on babel.
Note: This post assumes that you are comfortable with Docker, VSCode, and Typescript and/or babel. This post is not designed to help you understand how to compile Typescript or ES6 code, or how to run your development application in a Docker container. It is simply walking you through the configuration steps required to getting debugging working on an existing application.

First, let's enable the nodejs inspector

In order for any debugger (VSCode, Chrome, Jetbrains, etc) to debug a nodejs application, you need to enable the nodejs inspector.

To enable the nodejs inspector, it simply requires a command line argument when running node. Here is an example:

node --inspect=0.0.0.0:9229 server.js

Adding the --inspect argument will enable nodejs debugging so that a debugger can connect to it at the address: 0.0.0.0:9229.

Note: Because we are using the port 9229 to connect to the inspector, make sure that you are exposing and mapping your port 9229 in your Docker container to the host machine in Docker! I recommend mapping 9229 on the host to 9229 in the container to keep it easy and use the port 9229 everywhere.

This is great that we know how to enable the inspector, but we are using Typescript or ES6. Code that nodejs does not understand by default. This means we cannot directly run node against our source code! We need to transpile our source code first, then have nodejs run with the inspector enabled.

I will first be covering how to enable the nodejs inspector by using Babel. You may already be using babel yourself. If you're not using babel but instead am using the Typescript compiler module tsc, skip ahead to those docs farther down this post.

Enable the nodejs inspector using Babel

Babel is a fantastic tool for transpiling ES6 and Typescript code. It is also very easy to configure to get a nodejs inspector enabled.

We will simply be using babel-node module to transpile our Typescript or ES6 code and enabling the nodejs inspector, all in one step. babel-node has very similar syntax to to node, so all you need to do is add a command line argument to babel-node to enable the inspector.

Run a command such as the one below inside of your development Docker container to enable the nodejs inspector on your ES6 or Typescript code:

babel-node --inspect=0.0.0.0:9229 --extensions \".ts,.tsx\" server.ts
Note: If you're writing ES6 and not Typescript, (1) ignore the --extensions argument and change server.ts to server.js or whatever the entrypoint file of yours is.

Adding this command line argument is not enough, however. By default, babel will generate code that a debugger cannot map to breakpoints in the original source code. We need to configure babel to generate code the debugger can understand.

Open up your .babelrc file and add the following lines:

{
	"env": {
		"debug": {
			"sourceMaps": "inline",
			"retainLines": true
		}
	}
}

The 2 important lines here are sourceMaps and retainLines. I, however, map mine in an environment block, env, so I can enable source maps in development only by adding the environment variable, BABEL_ENV=debug. Leave out the environment variable in production and these 2 lines will be ignored.

Awesome! You are done and ready to move onto the next section on how to cofigure the VSCode debugger.

Enable the nodejs inspector using the Typescript compiler

To use the Typescript compiler to enable the nodejs inspector, we will do taking 2 individual steps. First, we will be using the Typescript compiler to transpile the Typescript code to an output directory. Second, we will be running node against the transpiled code in the output directory.

I am going to assume that you are already using the tsc module to compile your Typescript code. In order to configure Typescript for debugging, you need to generate source maps. All you need is to add the argument --sourceMap to your existing tsc command. (Instead of adding "sourceMaps": true to your .tsconfig.json file, I recommend adding it as a command line argument so that you can omit the argument for production transpiling.)

Here is a full example command you can run inside of your development Docker container to enable the nodejs inspector on your Typescript code:

tsc --sourceMap && node --inspect=0.0.0.0:9229 output-dirctory/server.js

Awesome! You are done and ready to move onto the next section on how to cofigure the VSCode debugger.

Configure VSCode debugger

Now that you have gotten the nodejs inspector enabled, you need to get the VSCode debugger to understand how to connect to node.

The configuration for VSCode's debugger is stored inside of .vscode/launch.json in your root project source code. This file is where you tell VSCode how to connect to nodejs.

Here is a sample launch.json file:


{
    "version": "0.2.0",
    "configurations": [     
      {    
        "type": "node",        
        "request": "attach",
        "name": "Docker development debug",
        "protocol": "inspector",
        "port": 9229,
        "address": "0.0.0.0",
        "sourceMaps": true,
        "localRoot": "${workspaceFolder}",
        "timeout": 20000,
        "stopOnEntry": false,        
        "remoteRoot": "/home/app/app/"
      }]
}

There is a lot here. I will allow you to check out the doc to learn more about this file, but the important things that you need to include would be:

  1. request: "attach" - Attach the debugger to the nodejs process. We are using this because of running our application inside of Docker so we must connect to the process remotely.
  2. port: 9229 address: "0.0.0.0" - the port number and address the nodejs inspector is running on. Make sure this aligns with what you entered as the command line argument, --inspect, above in this post.
  3. sourceMaps: true - Use the generated source maps created by babel or tsc. Because we are transpiling our code instead of running native nodejs javascript, we need to use this feature.
  4. localRoot, remoteRoot - Maps your local source code to the remote source code. This is required because we are running our code in a Docker container. {workspaceFolder} should be all you need for localRoot if your application's code is in the root of your folder project in VSCode. If you instead have your project split up into subdirectories (have package.json files located in a sub directory in your project's directory), then you can append the path to your subdirectory: {workspaceFolder}/path. remoteRoot maps to the directory inside of your Docker container that source code is sitting.
Note: If you followed the instructions above for using the Typescript compiler, tsc, to enable the nodejs inspector, make sure to also add one more configuration option to the launch.json file:
"outFiles": [
  "${workspaceFolder}/output-directory/**/*.js"
]
Make sure to change output-directory to the output directory you have set as the output directory for tsc.

If you have followed this post up to now, you should have debugging working! All you need to do now is to:

  1. Open up a source code file of yours and set a breakpoint inside of VSCode.
  2. Open up your terminal and run your development Docker container running node with the inspector enabled and the 9229 port exposed.
  3. In VSCode, click the green play button in the top left corner of the debugger mode:

You should then see in your terminal output the inspector starting and connecting in node. Execute your nodejs application to see if a breakpoint gets triggered.

If you are happy with this result, awesome! I am glad you got it working. If you want to enable some extra functionality, keep reading...

Run Docker and VSCode bugger with 1 button click

After you start using the VSCode debugger after a while, you will notice it can be a pain to go back and forth between your terminal window and VSCode window to start/stop your Docker container and VSCode debugger. It would be nice if you would (1) start your Docker container and attach the VSCode debugger in 1 step and (2) not have to switch back and forth between your terminal window and VSCode. Turns out, that is quite easy to do!

Tasks in VSCode are scripts that you want to execute inside of VSCode. Tasks are located in the file .vscode/tasks.json in your project folder.

Here is an example tasks.json file for starting our Docker container.

{
	"version": "2.0.0",
	"tasks": [{
		"label": "docker-dev-app",
		"command": "npm",
		"args": ["run", "dev:run:debug"],
		"type": "shell",
		"group": "build",
		"presentation": {
			"reveal": "always",
			"echo": true
		}
	}]
}

Well, it's up to you to create the npm script dev:run:debug in your package.json file 😄 to run your Docker command. This is a basic script that will run a npm script and show the output in the built-in terminal of VSCode. Note the label that you set as you will need that for the next step.

Note: An example npm script for dev:run:debug could be: docker-compose -f docker-compose.dev.yml up, for example. It could simply execute a Docker command.

If you wish to learn in more detail about the tasks.json file and what each line means, you can read more in the well made task docs.

Lastly, Add a line to your launch.json file: "preLaunchTask": "docker-dev-app" which asks to execute the task with the given label to execute.

Now, when you start the debugger with the gree play button in VSCode, your Docker container will start up, and have the debugger connect automatically. All with 1 button click.

Watch files for changes

After you start using the debugger often in your code, you will notice quickly how much of a pain it can be to (1) manually start your Docker container, (2) connect the debugger in VSCode, (3) do some debugging, (4) manually stop the debugger and Docker, (5) make code changes, (6+) then repeat. What if you could focus on just making changes to your code and then Docker and VSCode will automatically restart and connect together for you when you save your code changes?

Assuming that you followed the guide on running your application in VSCode with 1 button click, we do not have much else to do beyond that.

In launch.json, add 1 line: "restart": true to the file which indicates that the debugger should attempt to reconnect when it loses connection with node. This is important so when node restarts automatically for us, the debugger will also reconnect automatically for us.

In tasks.json, add the following lines:

"isBackground": true,
"problemMatcher": [{
	"pattern": [{
		"regexp": ".",
		"file": 1,
		"location": 2,
		"message": 3
	}],
	"background": {
		"activeOnStart": true,
		"beginsPattern": ".",
		"endsPattern": "."
	}
}]

isBackground: true is saying that we want our application to run in the background so that it can continue running, non-stop, in watch mode.

problemMatcher is designed to watch the console output of a command and report back problems to VSCode. You can read more about the purpose of them here in the docs. problemMatcher uses regex to report back problems and the state back to VSCode. If you are simply looking to get the debugger working without having to take the moment to write some regex, you can do the lazy method I did above that is using the wildcard "." in regex. Another resource that helped me with this is a stackoverflow question to understand why this works and how to improve on it.

Lastly, we need to modify the command line scripts that are being executed inside of our Docker container.

If you are using the Typescript compiler, tsc, go here. If using babel, go here.

Watch for changes using the Typescript compiler

When using the Typescript compiler, tsc, there is a very simple modification for you to change. Simply add the command line argument, --watch, to your tsc command and it will automatically watch for changes in your source code and recompile if needed.

tsc will watch and automatically generate javascript code on file change, but what about node? nodejs does not have an automatic reload functionality built-in. We have to use a separate tool to reload nodejs when code changes. A very popular tool that I would recommend is nodemon. nodemon will watch for file changes and execute a command you specify. It is tightly integrated with nodejs (but can be used with other langs) so it is simple to get going.

Simply change your node command that you have running after tsc and replace it with an equivalent nodemon command:

nodemon --watch output-dir --inspect=0.0.0.0:9229 output-dir/server.js

The command above is telling nodemon, watch for file changes that happen in the output-dir (make sure you change this to be the output directory you designated for tsc), turn on the nodejs inspector, and start nodejs running on the output-dir/server.js file.

Done! Now tsc will watch for when your Typescript code changes. When it does, it will transcompile javascript code into your output directory. Then when changes are detected in the output directory, nodejs will automatically stop and reload. The VSCode debugger is setup to reconnect automatically, so it will reconnect to nodejs when it's restarted.

Watch for changes using Babel

babel-node closely resembles nodejs. Nodejs does not have the functionality built-in to watch for code changes and restart when changes happen. We have to use a separate tool to reload nodejs when code changes. A very popular tool that I would recommend is nodemon. nodemon will watch for file changes and execute a command that is given to you. It is tightly integrated with nodejs (but can be used with other langs) so it is simple to get going.

Simply replace your babel-node command that you have already with the nodemon command:

npx nodemon app/server.ts

(This is assuming your entrypoint Typescript file is in the path, app/server.ts). nodemon does not understand Typescript by default. So we need to configure nodemon to know how to execute it.

To do that, create a file, nodemon.json, and put in the code below:

{
    "restartable": "rs",
    "ignore": [
      ".git",
      "node_modules/**/node_modules"
    ],
    "verbose": true,
    "execMap": {
      "ts": "babel-node --inspect=0.0.0.0:9229 --extensions \".ts,.tsx\""
    },
    "watch": [
      "app/"
    ],
    "ext": "js,json,ts,tsx"
  }

Looking specifically at the execMap block, you see ts. That is the file extension of a file and then gives a command telling nodemon, "When you have a file with this extension, this is how you execute it". We put our babel-node command we had before inside of there.

Done! Now nodemon will watch for when your Typescript code changes. When it does, it will automatically stop and restart babel-node. The VSCode debugger is setup to reconnect automatically now, so it will reconnect to nodejs when it's restarted.

Misc

Some last pieces of information that you might find helpful...

Debug a launch.json file

If you encounter problems with getting the VSCode debugger connected to your application after following this post, there is some hope for you.

If you add "trace": true to your launch.json file, launch your debugger as you normally would, then in the "Debug console" terminal window of VSCode, it will give you a path to a text file. VSCode is logging debug output into that file for you. You can open that file and read the contents inside to see if you can detect what is going wrong with the debugger connecting.

Run babel-node and lint Typescript at the same time

If you use babel-node and Typescript, I tend to have tsc and babel-node running together inside of my Docker container. babel-node does not report compilation errors to you. It will attempt to compile everything no matter what. This is why I enjoy having tsc running alongside babel-node.

To do this, I use the npm module, concurrently, inside of my development Docker container. As a TL;DR for concurrently, here is an example command line script I use in my Docker container:

concurrently --kill-others --names \"lint,node\" -c \"blue,green\" \"npx tsc --noEmit --pretty --watch\" \"npx nodemon app/server.ts\""

I hope you enjoyed this post! It takes a bit of explaining to get all of this working together. There is a lot to learn and configure, but I hope taking the time to read this post taught you about how and why this all works so you understand it better.

All of these tools are very flexible and powerful as you can see. That is one reason I enjoy writing Typescript, nodejs applications. The web community seems to understand we enjoying integrating our tools together to create a more enjoyable developer experience.

Levi