{{announcement.body}}
{{announcement.title}}

Debug a Node.js Application Running in a Docker Container

DZone 's Guide to

Debug a Node.js Application Running in a Docker Container

This article shows how you can debug a simple Node.js application running in a Docker container. Use this tutorial as a reference while building your own!

· Web Dev Zone ·
Free Resource

This blog post shows how you can debug a simple Node.js application running in a Docker container. The tutorial is laid out in a fashion that allows you to use it as a reference while you’re building your own Node.js application and is intended for readers who have prior exposure to JavaScript programming and Docker.

Prerequisites

  1. Docker. For details about installing Docker, refer to the Install Docker page.
  1. Node.js 10 or higher. To check out if Node.js is installed on your computer, fire up a terminal window and type the following command:
Shell
 




x


 
1
node -v


If Node.js is already installed, you'll see something like the following: 

Shell
 




xxxxxxxxxx
1


 
1
v10.15.3


If Node.js is not installed, you can download the installer from the Download page. 

  1. Microsoft Visual Studio. For details on how to install Visual Studio, refer to the Install Visual Studio page.

Initializing Your Todo Application

For the scope of this tutorial, we’ll create a bare-bones todo list that allows users to add and delete tasks. There will be a small bug in the application and we’ll use Visual Studio Code to debug the code and fix the issue. The knowledge you’ll acquire in this tutorial will help you debug your own applications. Let’s get started.

  1. Fire up a new terminal window, move into your projects directory, and then execute the following command:
Shell
 




xxxxxxxxxx
1


 
1
mkdir MyTodoApp && cd MyTodoApp


2. Initialize the project with: 

Shell
 




xxxxxxxxxx
1


 
1
npm init -y


This will output something like the following: 

Shell
 




xxxxxxxxxx
1
14


 
1
Wrote to /Users/ProspectOne/Documents/MyTodoApp/package.json:
2
 
          
3
{
4
  "name": "MyTodoApp",
5
  "version": "1.0.0",
6
  "description": "",
7
  "main": "index.js",
8
  "scripts": {
9
    "test": "echo \"Error: no test specified\" && exit 1"
10
  },
11
  "keywords": [],
12
  "author": "",
13
  "license": "ISC"
14
}


Creating a Bare-bones Todo Application

We’ll build our todo application using Express, a fast, unopinionated, minimalist web framework for Node.js. Express was designed to make developing websites much easier and it’s one of the most popular Node.js web frameworks.

  1. Install express and a few other prerequisites by entering the following command:
Shell
 




xxxxxxxxxx
1


 
1
npm install express body-parser cookie-session ejs  --save


Shell
 




xxxxxxxxxx
1
15


 
1
> ejs@3.0.1 postinstall /Users/ProspectOne/Documents/test/MyTodoApp/node_modules/ejs
2
> node ./postinstall.js
3
 
          
4
Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)
5
 
          
6
npm notice created a lockfile as package-lock.json. You should commit this file.
7
npm WARN MyTodoApp@1.0.0 No description
8
npm WARN MyTodoApp@1.0.0 No repository field.
9
 
          
10
+ ejs@3.0.1
11
+ body-parser@1.19.0
12
+ express@4.17.1
13
+ cookie-session@1.3.3
14
added 55 packages from 39 contributors and audited 166 packages in 6.533s
15
found 0 vulnerabilities


2. Create a file called app.js with the following content: 

Shell
 




xxxxxxxxxx
1
36


 
1
const express = require('express')
2
const app = express()
3
const bodyParser = require('body-parser')
4
const session = require('cookie-session')
5
const urlencodedParser = bodyParser.urlencoded({ extended: false })
6
const port = 3000
7
 
          
8
app.use(session({ secret: process.env.SECRET }))
9
  .use(function (req, res, next) {
10
    next()
11
  })
12
 
          
13
  .get ('/todo', function (req, res) {
14
    res.render('todo.ejs', { todolist: req.session.todolist })
15
  })
16
 
          
17
  .post ('/todo/add/', urlencodedParser, function (req, res) {
18
    if (req.body.newtodo != '') {
19
      req.session.todolist.push(req.body.newtodo)
20
    }
21
    res.redirect('/todo')
22
  })
23
 
          
24
  .get ('/todo/delete/:id', function (req, res) {
25
    if (req.params.id != '') {
26
      req.session.todolist.splice(req.params.id, 1)
27
    }
28
    res.redirect('/todo')
29
  })
30
 
          
31
  .use (function (req, res, next) {
32
    res.redirect('/todo')
33
  })
34
 
          
35
 
          
36
  .listen(port, () => console.log(`MyTodo app is listening on port ${port}!`))



Note that the above snippet is a derivative work of the code from the openclassroom.com website and explaining how this code works is beyond the scope of this tutorial. If the details are fuzzy, we recommend you check out their site to further your learning after you finish this tutorial.

  1. Create a file called ./views/todo.ejs and paste into it the following content:
Shell
 




xxxxxxxxxx
1
28


 
1
<!DOCTYPE html>
2
 
          
3
<html>
4
    <head>
5
        <title>My todolist</title>
6
        <style>
7
            a {text-decoration: none; color: black;}
8
        </style>
9
    </head>
10
 
          
11
    <body>
12
        <h1>My todolist</h1>
13
 
          
14
        <ul>
15
        <% todolist.forEach(function(todo, index) { %>
16
            <li><a href="/todo/delete/<%= index %>">✘</a> <%= todo %></li>
17
        <% }); %>
18
        </ul>
19
 
          
20
        <form action="/todo/add/" method="post">
21
            <p>
22
                <label for="newtodo">What should I do?</label>
23
                <input type="text" name="newtodo" id="newtodo" autofocus />
24
                <input type="submit" />
25
            </p>
26
        </form>
27
    </body>
28
</html>


4. At this point, your directory structure should look something like the following: 

Shell
 




xxxxxxxxxx
1


1
tree -L 2 -I node_modules



Shell
 




xxxxxxxxxx
1


 
1
.
2
├── app.js
3
├── package-lock.json
4
├── package.json
5
└── views
6
    └── todo.ejs
7
 
          
8
1 directory, 4 files



5. Now you are ready to start your web server by entering: 

Shell
 




xxxxxxxxxx
1


 
1
SECRET=bestkeptsecret; node app.js


This will print out the following message to the console: 

Shell
 




xxxxxxxxxx
1


 
1
MyTodo app is listening on port 3000!



Create a Docker Image

Now that you’ve written the Todo application, it's time to add create a Docker image for it. Each Docker container is based on a Docker image that contains all the information needed to deploy and run your app with Docker. To run a Docker container you can:

  • Download an existing Docker image
  • Create your own image

In this tutorial, you'll create your own image. Note that a Docker image is usually comprised of multiple layers and each layer is basically a read-only filesystem. The way this works is that Docker creates a layer for each instruction found in the Dockerfile and places it atop of the previous layers. It is considered good practice to place the application’s code, that changes often, closer to the bottom of the file.

  1. Create a file called Dockerfile and paste the following snippet into it:
Shell
 




xxxxxxxxxx
1


 
1
FROM node:10
2
WORKDIR /usr/src/app
3
COPY package*.json ./
4
RUN npm install
5
COPY . .
6
EXPOSE 3000
7
CMD [ "node", "app.js" ]



Let’s take a closer look at this file:

  • FROM: sets the base image. Everything you’ll add later on it’ll be based on this image. In this example, we’re using Node.js version 10.
  • WORKDIR: this command sets the working directory that’ll be used for the COPY, RUN, and CMD commands.
  • RUN: this line of code runs the npm install command inside your Docker container.
  • COPY: copies files from the build context into the Docker image
  • EXPOSE: specifies that a process running inside the container is listening to the 3000 port. This will be useful later in this tutorial when you’ll forward ports from the host to the container.
  • CMD: this line runs the node app.js inside your Docker container only after the container has been started.
  1. To avoid sending large files to the build context and speed up the process, you can use a .dockerignore file. This is nothing more than a plain text file that contains the name of the files and directories that should be excluded from the build. You can think of it as something similar to a .gitignore file. Create a file called .dockerignore with the following content:
Shell
 




xxxxxxxxxx
1


 
1
node_modules
2
npm-debug.log


  1. Now you can go ahead and build your Docker image by entering the docker build command followed by:
  • The -t parameter which specifies the name of the image
  • The path to the context which should point to the set of files you want to reference from your Dockerfile
Shell
 




xxxxxxxxxx
1


1
docker build -t prospectone/my-todo-list .



Shell
 




xxxxxxxxxx
1
36


1
Sending build context to Docker daemon  24.58kB
2
Step 1/7 : FROM node:10
3
 ---> c5d0d6dc0b5b
4
Step 2/7 : WORKDIR /usr/src/app
5
 ---> Using cache
6
 ---> 508b797a892e
7
Step 3/7 : COPY package*.json ./
8
 ---> 0b821f725c19
9
Step 4/7 : RUN npm install
10
 ---> Running in d692a6278d2b
11
 
          
12
> ejs@3.0.1 postinstall /usr/src/app/node_modules/ejs
13
> node ./postinstall.js
14
 
          
15
Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)
16
 
          
17
npm WARN MyTodoApp@1.0.0 No description
18
npm WARN MyTodoApp@1.0.0 No repository field.
19
 
          
20
added 55 packages from 39 contributors and audited 166 packages in 2.564s
21
found 0 vulnerabilities
22
 
          
23
Removing intermediate container d692a6278d2b
24
 ---> 067de030e269
25
Step 5/7 : COPY . .
26
 ---> 3141ccb6e094
27
Step 6/7 : EXPOSE 3000
28
 ---> Running in eb824e38d8c6
29
Removing intermediate container eb824e38d8c6
30
 ---> b09d55adc1c4
31
Step 7/7 : CMD [ "node", "app.js" ]
32
 ---> Running in 7e77e0cbfa75
33
Removing intermediate container 7e77e0cbfa75
34
 ---> c0a2db4c7a65
35
Successfully built c0a2db4c7a65
36
Successfully tagged prospectone/my-todo-list:latest


As mentioned above, the way the docker build command works is that it adds a new layer for each command in your Dockerfile. Then, once a command is successfully executed, Docker deletes the intermediate container.

  1. Now that you’ve built your image, let’s run it by entering the docker run command and passing it the following arguments:
  • -p with the port on the host (3001) that’ll be forwarded to the container (3000), separated by :
  • -e to create an environment variable called SECRET and set its value to bestkeptsecret
  • -d to specify that the container should be run in the background
  • The name of the image (prospectone/my-awesome-app)
Shell
 




xxxxxxxxxx
1


 
1
docker run -p 3001:3000 -e SECRET=bestkeptsecret -d prospectone/my-todo-list



Shell
 




xxxxxxxxxx
1


 
1
db16ed662e8a3e0a93f226ab873199713936bd687a4546d2fce93e678d131243


5. You can verify that your Docker container is running with: 

Shell
 




xxxxxxxxxx
1


 
1
docker ps


The output should be similar to: 

Shell
 




xxxxxxxxxx
1


 
1
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                    NAMES
2
a6eb166191c7        prospectone/my-todo-list   "docker-entrypoint.s…"   4 seconds ago       Up 3 seconds        0.0.0.0:3001->3000/tcp   happy_hawking



6. To inspect the logs, enter the docker logs command followed by the id of your container: 

Shell
 




xxxxxxxxxx
1


1
docker logs a6eb166191c7


Shell
 




xxxxxxxxxx
1


 
1
MyTodo app is listening on port 3000!



7. Now that your application is up and running, point your browser to http://localhost:3001 and let us add a new todo. As you can see below, the application errors out on line 15 of the todo.ejs file: 


In the next sections, you’ll learn how to debug this using Visual Studio Code.

  1. But first, stop the container with:
Shell
 




xxxxxxxxxx
1


 
1
docker kill a6eb166191c7



Shell
 




xxxxxxxxxx
1


1
a6eb166191c7



Enable Debugging in Microsoft Visual Studio Code

Visual Studio Code provides debugging support for the Node.js applications running inside a Docker container. Follow the next steps to enable this feature:

  1. Edit your Dockerfile by replacing the following line:
Shell
 




xxxxxxxxxx
1


 
1
CMD [ "node", "app.js" ]


with:

Shell
 




xxxxxxxxxx
1


 
1
CMD [ "npm", "run", "start-debug" ]


Your Dockerfile should look something like the following:

Shell
 




xxxxxxxxxx
1


1
FROM node:10
2
WORKDIR /usr/src/app
3
COPY package*.json ./
4
RUN npm install
5
COPY . .
6
EXPOSE 3000
7
CMD [ "npm", "run", "start-debug" ]


2. Open the package.json file and add the following line to the scripts object: 

Shell
 




xxxxxxxxxx
1


 
1
"start-debug": "node --inspect=0.0.0.0 app.js"



This line of code starts the Node.js process and listens for a debugging client on port 9229.

Here’s how your package.json file should look like:

Shell
 




xxxxxxxxxx
1
19


 
1
{
2
  "name": "MyTodoApp",
3
  "version": "1.0.0",
4
  "description": "",
5
  "main": "index.js",
6
  "scripts": {
7
    "test": "echo \"Error: no test specified\" && exit 1",
8
    "start-debug": "node --inspect=0.0.0.0 app.js"
9
  },
10
  "keywords": [],
11
  "author": "",
12
  "license": "ISC",
13
  "dependencies": {
14
    "body-parser": "^1.19.0",
15
    "cookie-session": "^1.3.3",
16
    "ejs": "^3.0.1",
17
    "express": "^4.17.1"
18
  }
19
}



3. Every time the Dockerfile gets updated, you must build again your Docker image: 

Shell
 




xxxxxxxxxx
1


1
docker build -t prospectone/my-todo-list .



Shell
 




xxxxxxxxxx
1
30


 
1
Sending build context to Docker daemon  19.97kB
2
Step 1/7 : FROM node:10
3
 ---> c5d0d6dc0b5b
4
Step 2/7 : WORKDIR /usr/src/app
5
 ---> Using cache
6
 ---> 508b797a892e
7
Step 3/7 : COPY package*.json ./
8
 ---> c0eec534b176
9
Step 4/7 : RUN npm install
10
 ---> Running in a155901cb957
11
npm WARN MyAwesomeApp@1.0.0 No description
12
npm WARN MyAwesomeApp@1.0.0 No repository field.
13
 
          
14
added 50 packages from 37 contributors and audited 126 packages in 11.504s
15
found 0 vulnerabilities
16
 
          
17
Removing intermediate container a155901cb957
18
 ---> 010473a35e41
19
Step 5/7 : COPY . .
20
 ---> 76dfa12d4db4
21
Step 6/7 : EXPOSE 3000
22
 ---> Running in b5a334c9a2ea
23
Removing intermediate container b5a334c9a2ea
24
 ---> b5a869ab5441
25
Step 7/7 : CMD [ "npm", "run", "start-debug" ]
26
 ---> Running in 1beb2ca9a391
27
Removing intermediate container 1beb2ca9a391
28
 ---> 157b7d4cb77b
29
Successfully built 157b7d4cb77b
30
Successfully tagged prospectone/my-todo-list:latest



Note that Step 7 has been updated, meaning that Docker will now execute the npm run start-debug command.

  1. To enable debugging with Visual Studio Code, you must also forward port 9229. Start your Docker container by entering:
Shell
 




xxxxxxxxxx
1


 
1
docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret22222 -d perfops/my-todo-list


Shell
 




xxxxxxxxxx
1


1
0f5860bebdb5c70538bcdd10ddc901411b37ea0c7d92283310700085b1b8ddc5



5. You can inspect the logs by entering the docker logs command followed the id of your container: 

Shell
 




xxxxxxxxxx
1


1
docker logs 0f5860bebdb5c70538bcdd10ddc901411b37ea0c7d92283310700085b1b8ddc5



Shell
 




xxxxxxxxxx
1


1
> My@1.0.0 start-debug /usr/src/app
2
> node --inspect=0.0.0.0 app.js
3
 
          
4
Debugger listening on ws://0.0.0.0:9229/59d4550c-fc0e-412e-870a-c02b4a6dcd0f
5
For help, see: https://nodejs.org/en/docs/inspector



Note that the debugger is now listening to port 9229. Next, you’ll configure Visual Studio code to debug your application.

Debug Your Application with Visual Studio Code

  1. In Visual Studio Code, open the MyTodoApp directory.
  1. The configuration for debugging is stored in a file called launch.json. To open it, press Command+Shift+P and then choose Debug: Open launch.json.
  1. Replace the content of the launch.json file with the following snippet:
Shell
 




xxxxxxxxxx
1
19


 
1
{
2
  "version": "0.2.0",
3
  "configurations": [
4
      {
5
          "name": "Docker: Attach to Node",
6
          "type": "node",
7
          "request": "attach",
8
          "port": 9229,
9
          "address": "localhost",
10
          "localRoot": "${workspaceFolder}",
11
          "remoteRoot": "/usr/src/app",
12
          "protocol": "inspector",
13
          "skipFiles": [
14
            "${workspaceFolder}/node_modules/**/*.js",
15
            "<node_internals>/**/*.js"
16
          ]
17
      }
18
  ]
19
}



Note that we’re using the skipFiles attribute to avoid stepping through the code in the node_modules directory and the built-in core modules of Node.js.

  1. Now everything is set up and you can start debugging your application. Remember that there was an error at line 15 in the views.js file, which basically iterates over the todolist array: todolist.forEach(function(todo, index). Looking at the app.js file you’ll see that todo.ejs gets rendered at line 14. Let’s add a breakpoint so we can inspect the value of the todolist variable:
  1. Enter Shift+Command+D to switch to the Debug view. Then, click the Debug and Run button:
  1. To inspect the value of the req.session.todolist variable, you must add a new expression to watch by selecting the + sign and then typing the name of the variable (req.session.todolist):
  1. Switch to the browser window and reload the http://localhost:3001 page.

Note the Waiting for localhost message at the bottom. This means that our breakpoint has paused execution and we can inspect the value of the req.session.todolist variable. Move back to Visual Studio to get details:


So the req.session.todolist variable is undefined. Can you think of how you could fix this bug? The answer is below, but don’t continue until you’ve given it some thought.

  1. The ejb template iterates over the todolist array which should be stored in the current session. But we forgot to initialize this array so it’s undefined. Let’s fix that by adding the following lines of code to the .use function :
Shell
 




xxxxxxxxxx
1


 
1
if (typeof (req.session.todolist) == 'undefined') {
2
    req.session.todolist = []
3
}


Make sure you paste this snippet just above the line of code that calls the next function. Your .use function should look like below:

Shell
 




xxxxxxxxxx
1


 
1
app.use(session({ secret: process.env.SECRET }))
2
  .use(function (req, res, next) {
3
    if (typeof (req.session.todolist) == 'undefined') {
4
      req.session.todolist = []
5
    }
6
    next()
7
  })



9. Retrieve the id of your running container : 

Shell
 




xxxxxxxxxx
1


 
1
docker ps



Shell
 




xxxxxxxxxx
1


 
1
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                                            NAMES
2
cb9f175f7af3        prospectone/my-todo-list   "docker-entrypoint.s…"   15 minutes ago      Up 15 minutes       0.0.0.0:9229->9229/tcp, 0.0.0.0:3001->3000/tcp   nervous_hopper



10. Stop the container by entering the docker kill command followed by its id:

Shell
 




xxxxxxxxxx
1


 
1
docker kill cb9f175f7af3


 

Shell
 




xxxxxxxxxx
1


 
1
cb9f175f7af3



11. To apply the changes you must run the docker build command again: 

Shell
 




xxxxxxxxxx
1


 
1
docker build -t prospectone/my-todo-list .



Shell
 




xxxxxxxxxx
1
24


 
1
Sending build context to Docker daemon  26.11kB
2
Step 1/7 : FROM node:10
3
 ---> c5d0d6dc0b5b
4
Step 2/7 : WORKDIR /usr/src/app
5
 ---> Using cache
6
 ---> 508b797a892e
7
Step 3/7 : COPY package*.json ./
8
 ---> Using cache
9
 ---> c5ac875da76b
10
Step 4/7 : RUN npm install
11
 ---> Using cache
12
 ---> 29e7b3bac403
13
Step 5/7 : COPY . .
14
 ---> b92f577afd57
15
Step 6/7 : EXPOSE 3000
16
 ---> Running in 78606a3c2e03
17
Removing intermediate container 78606a3c2e03
18
 ---> 59c2ed552549
19
Step 7/7 : CMD [ "npm", "run", "start-debug" ]
20
 ---> Running in e0313973bb5a
21
Removing intermediate container e0313973bb5a
22
 ---> 70a675646c0d
23
Successfully built 70a675646c0d
24
Successfully tagged prospectone/my-todo-list:latest



12. Now you can run the container with: 

Shell
 




xxxxxxxxxx
1


1
docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret222212 -d prospectone/my-todo-list



Shell
 




xxxxxxxxxx
1


 
1
f75d4ef8b702df13749b10615f3945ea61b36571b0dc42b76f50b3c99e14f4c6



13. Inspect the logs by running the following command: 

Shell
 




xxxxxxxxxx
1


 
1
docker logs 10f467dbb476



Shell
 




xxxxxxxxxx
1


 
1
f75d4ef8b702df13749b10615f3945ea61b36571b0dc42b76f50b3c99e14f4c6



14. Reload the page and add a new task: 

Congratulations, you’ve successfully written a bare-bones todo app, ran it inside a Docker container, and used Visual Studio Code to debug it and fix a bug. In the next article, we’ll walk you through the process of dockerizing an existing application.

Topics:
cloud (add topic), docker, kubernetes, node.js, serverless, visual studio

Published at DZone with permission of Sudip Sengupta . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}