Published 

16

 

Feb 2023

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

Subscribe to learn about new product features, the latest in technology, solutions, and updates.

Nodejs

Docker

Docker Compose

Debugging

Visual Studio Code

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

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 !!--inspect!! to 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.json!! and 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": [
                "/**"
            ],
            "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.

James Harrison
Solution Architect, FL0

Copy link

Our blog

Latest blog posts

Tool and strategies modern teams need to help their companies grow.

View all posts

View all posts

ready to ship

We’re excited to see you launch your next big idea.

Get started for free

arrow right