The best way to debug a Node.js app running in a Docker container

James Harrison
FL0 Engineering
6 min readFeb 15, 2023

--

In my previous article The perfect multi-stage Dockerfile for Node.js apps I explained how to set up a Dockerfile that would work in both a local environment with hot-reloading and in production as a minified image.

This article extends from where we left off, but adds a critical element: debugging!

Before continuing, please have a read of the previous article or clone the example repository (be sure to check out the branch called multi-stage-dockerfile)!

We’re going to cover the following:

  1. Updating package.json with a debug script
  2. Creating a debug version of our docker-compose.yml file
  3. Attaching a debugger to the container
  4. Automating Visual Studio Code to start and stop containers
The Docker whale being inspected by engineers in a warehouse (thanks, AI)

Deploying to the cloud

If you want a really simple way to deploy your container to a scalable infrastructure, check out FL0! It’s a platform that makes it as simple as possible to go from code to cloud, complete with dev/prod environments, databases and more. All you have to do is commit your code and FL0 will handle the rest.

Creating the debug script

Open up package.json and take a look at the scripts section. There’s currently a start and a start:dev option. Our Dev script uses Nodemon to watch for changes and reload the server as needed. Nodemon also accepts a flag called --inspectto run in debug mode. If you’re interested in some thrilling reading, learn more in the Node.js docs!

Open up your package.json file and add a new script called start:debug.

{
...
"scripts": {
...
"start:debug": "nodemon -r dotenv/config --inspect=0.0.0.0 src/index.js",
},
...
}

It’s the same as our start:dev script, but we pass the --inspect flag and the IP address 0.0.0.0, meaning we are allowing debugger connections from any IP address.

Note: If you try and run this script in your terminal, you’ll get an error that the database can’t be found. It needs to be run with Docker Compose so that the database is also provisioned.

Creating a debug Docker Compose file

Docker Compose has a great feature called overrides which allows you to have multiple docker-compose.yml files in your codebase, one overriding parts of the other. This means you can have a base docker-compose.yml and a docker-compose.debug.yml file that overrides things like the port and the start command. Let’s go and set that up!

Create a new file in your repo called docker-compose.debug.yml and paste in the following:

version: '3.4'

services:
app:
ports:
- 3000:80
- 9229:9229
command: ["npm", "run", "start:debug"]

You can see it’s pretty minimal. All it does is override the ports and command section of our main file. And the command we’re running is our newly created start:debug command! The port 9229 is the default port for debugging with Node.js, and we map that from the container to our host machine so that our IDE can connect properly.

If we ran docker compose up right now it would only use our original YAML file. But if we run the following, it will use both files:

$ docker compose -f docker-compose.yml -f docker-compose.debug.yml up

Go ahead and try that out! In the terminal output you should see a couple of important lines that indicate Node was started in debug mode successfully:

[nodemon] starting `node -r dotenv/config --inspect=0.0.0.0 src/index.js`
Debugger listening on ws://0.0.0.0:9229/51b990e4-17fd-40ef-9a4c-784f033329d2

If you see that, we’re kicking goals! If not, maybe scroll through Instagram for a while and see if it fixes itself. If you’re really stuck, leave a comment below and I’ll get back to you!

Attaching a debugger to the container

While there are lots of available debuggers, we’re going to focus on Visual Studio Code (VSC) in this article. With your containers up and running in Debug mode, create a folder and file in the root of your repo called .vscode/launch.jsonand add this content:

{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [


]
}

With this file open, click cmd+shift+p (ctrl+shift+p) to open the command palette and select an option called Debug: Add Configuration.... From the list, select Node.js: Attach to Remote Program.

Using the command palette to add a launch configuration

You should see some JSON added to your launch.json file. Modify the file as follows:

  1. Set the address property to “localhost” as we have mapped port 9229 from our container to our host machine
  2. Set the remoteRoot property to /usr/src/app as this is our Dockerfile’s WORKDIR and contains all our source code (inside the container)
  3. Set a new property called restart so that when we make a code change and nodemon restarts the server, we don’t lose our debugger connection

Your file should look like this:

{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"address": "localhost",
"localRoot": "${workspaceFolder}",
"name": "Attach to Remote",
"port": 9229,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true
}
]
}

Once you save the file you’ll see an option in the Debug panel to launch your configuration. Go ahead and click it! Just be sure your containers are running in debug mode first.

Launching our new debug configuration

If all goes well you should see an orange bar at the bottom of VSC. Open index.js and set a couple of breakpoints in the request handlers:

Setting breakpoints in index.js

In your browser, load up http://localhost:3000/. If your debugger is working, it should pause at the breakpoints you just set. Congratulations, you just attached a debugger to a Docker container! But don’t get overly attached just yet, we still have more to do…

Automating Visual Studio Code to start and stop containers

What we’ve got so far is great, but it requires some manual steps. Starting our containers, running the debugger, disconnecting the debugger, stopping the containers. If you’re happy with this…that’s fine! It definitely gives you the most control. But if you’d like to automate these steps, read on…

Create a new file in your .vscode folder called tasks.json and update it to look like this:

{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "docker-compose",
"label": "docker-compose: debug",
"dockerCompose": {
"up": {
"detached": true,
"build": true
},
"files": [
"${workspaceFolder}/docker-compose.yml",
"${workspaceFolder}/docker-compose.debug.yml"
]
}
},
{
"type": "docker-compose",
"label": "docker-compose: down",
"dockerCompose": {
"down": {}
}
}
]
}

The first task called docker-compose: debug will start our containers using the docker-compose.debug.yml configuration, and the second will stop them again. Creating tasks like this means we can call them from our launch.json configuration. Open up your launch config and add a couple of new lines:

{
...
"configurations": [
{
...
"preLaunchTask": "docker-compose: debug",
"postDebugTask": "docker-compose: down"
}
]
}

These new lines will run before and after the debugger starts, meaning our containers will start and stop automatically! Go ahead and try it out, but make sure your containers are stopped first. Check out the video below to see it in action.

Thanks for reading! I’d love to hear your questions, suggestions and feedback so please don’t hesitate to leave a comment.

--

--