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.
node -v
If Node.js is already installed, you’ll see something like the following:
v10.15.3
If Node.js is not installed, you can download the installer from the Download page.
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.
mkdir MyTodoApp && cd MyTodoApp
npm init -y
This will output something like the following:
Wrote to /Users/ProspectOne/Documents/MyTodoApp/package.json: { "name": "MyTodoApp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
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.
express
and a few other prerequisites by entering the following command:npm install express body-parser cookie-session ejs --save
> ejs@3.0.1 postinstall /Users/ProspectOne/Documents/test/MyTodoApp/node_modules/ejs > node ./postinstall.js Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/) npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN MyTodoApp@1.0.0 No description npm WARN MyTodoApp@1.0.0 No repository field. + ejs@3.0.1 + body-parser@1.19.0 + express@4.17.1 + cookie-session@1.3.3 added 55 packages from 39 contributors and audited 166 packages in 6.533s found 0 vulnerabilities
app.js
with the following content:const express = require('express') const app = express() const bodyParser = require('body-parser') const session = require('cookie-session') const urlencodedParser = bodyParser.urlencoded({ extended: false }) const port = 3000 app.use(session({ secret: process.env.SECRET })) .use(function (req, res, next) { next() }) .get ('/todo', function (req, res) { res.render('todo.ejs', { todolist: req.session.todolist }) }) .post ('/todo/add/', urlencodedParser, function (req, res) { if (req.body.newtodo != '') { req.session.todolist.push(req.body.newtodo) } res.redirect('/todo') }) .get ('/todo/delete/:id', function (req, res) { if (req.params.id != '') { req.session.todolist.splice(req.params.id, 1) } res.redirect('/todo') }) .use (function (req, res, next) { res.redirect('/todo') }) .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.
./views/todo.ejs
and paste into it the following content:<!DOCTYPE html> <html> <head> <title>My todolist</title> <style> a {text-decoration: none; color: black;} </style> </head> <body> <h1>My todolist</h1> <ul> <% todolist.forEach(function(todo, index) { %> <li><a href="/todo/delete/<%= index %>">✘</a> <%= todo %></li> <% }); %> </ul> <form action="/todo/add/" method="post"> <p> <label for="newtodo">What should I do?</label> <input type="text" name="newtodo" id="newtodo" autofocus /> <input type="submit" /> </p> </form> </body> </html>
tree -L 2 -I node_modules
. ├── app.js ├── package-lock.json ├── package.json └── views └── todo.ejs 1 directory, 4 files
SECRET=bestkeptsecret; node app.js
This will print out the following message to the console:
MyTodo app is listening on port 3000!
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:
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.
Dockerfile
and paste the following snippet into it:FROM node:10 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD [ "node", "app.js" ]
Let’s take a closer look at this file:
npm install
command inside your Docker container.node app.js
inside your Docker container only after the container has been started..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:node_modules npm-debug.log
docker build
command followed by:-t
parameter which specifies the name of the imagedocker build -t prospectone/my-todo-list .
Sending build context to Docker daemon 24.58kB Step 1/7 : FROM node:10 ---> c5d0d6dc0b5b Step 2/7 : WORKDIR /usr/src/app ---> Using cache ---> 508b797a892e Step 3/7 : COPY package*.json ./ ---> 0b821f725c19 Step 4/7 : RUN npm install ---> Running in d692a6278d2b > ejs@3.0.1 postinstall /usr/src/app/node_modules/ejs > node ./postinstall.js Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/) npm WARN MyTodoApp@1.0.0 No description npm WARN MyTodoApp@1.0.0 No repository field. added 55 packages from 39 contributors and audited 166 packages in 2.564s found 0 vulnerabilities Removing intermediate container d692a6278d2b ---> 067de030e269 Step 5/7 : COPY . . ---> 3141ccb6e094 Step 6/7 : EXPOSE 3000 ---> Running in eb824e38d8c6 Removing intermediate container eb824e38d8c6 ---> b09d55adc1c4 Step 7/7 : CMD [ "node", "app.js" ] ---> Running in 7e77e0cbfa75 Removing intermediate container 7e77e0cbfa75 ---> c0a2db4c7a65 Successfully built c0a2db4c7a65 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.
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 backgroundprospectone/my-awesome-app
)docker run -p 3001:3000 -e SECRET=bestkeptsecret -d prospectone/my-todo-list
db16ed662e8a3e0a93f226ab873199713936bd687a4546d2fce93e678d131243
docker ps
The output should be similar to:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a6eb166191c7 prospectone/my-todo-list "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 0.0.0.0:3001->3000/tcp happy_hawking
docker logs
command followed by the id
of your container:docker logs a6eb166191c7
MyTodo app is listening on port 3000!
todo.ejs
file:In the next sections, you’ll learn how to debug this using Visual Studio Code.
docker kill a6eb166191c7
a6eb166191c7
Visual Studio Code provides debugging support for the Node.js applications running inside a Docker container. Follow the next steps to enable this feature:
CMD [ "node", "app.js" ]
with:
CMD [ "npm", "run", "start-debug" ]
Your Dockerfile should look something like the following:
FROM node:10 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD [ "npm", "run", "start-debug" ]
package.json
file and add the following line to the scripts
object:"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:
{ "name": "MyTodoApp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start-debug": "node --inspect=0.0.0.0 app.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "body-parser": "^1.19.0", "cookie-session": "^1.3.3", "ejs": "^3.0.1", "express": "^4.17.1" } }
docker build -t prospectone/my-todo-list .
Sending build context to Docker daemon 19.97kB Step 1/7 : FROM node:10 ---> c5d0d6dc0b5b Step 2/7 : WORKDIR /usr/src/app ---> Using cache ---> 508b797a892e Step 3/7 : COPY package*.json ./ ---> c0eec534b176 Step 4/7 : RUN npm install ---> Running in a155901cb957 npm WARN MyAwesomeApp@1.0.0 No description npm WARN MyAwesomeApp@1.0.0 No repository field. added 50 packages from 37 contributors and audited 126 packages in 11.504s found 0 vulnerabilities Removing intermediate container a155901cb957 ---> 010473a35e41 Step 5/7 : COPY . . ---> 76dfa12d4db4 Step 6/7 : EXPOSE 3000 ---> Running in b5a334c9a2ea Removing intermediate container b5a334c9a2ea ---> b5a869ab5441 Step 7/7 : CMD [ "npm", "run", "start-debug" ] ---> Running in 1beb2ca9a391 Removing intermediate container 1beb2ca9a391 ---> 157b7d4cb77b Successfully built 157b7d4cb77b Successfully tagged prospectone/my-todo-list:latest
Note that the Step 7 has been updated, meaning that Docker will now execute the npm run start-debug
command.
9229
. Start your Docker container by entering:docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret22222 -d perfops/my-todo-list
0f5860bebdb5c70538bcdd10ddc901411b37ea0c7d92283310700085b1b8ddc5
docker logs
command followed the id
of your container:docker logs 0f5860bebdb5c70538bcdd10ddc901411b37ea0c7d92283310700085b1b8ddc5
> My@1.0.0 start-debug /usr/src/app > node --inspect=0.0.0.0 app.js Debugger listening on ws://0.0.0.0:9229/59d4550c-fc0e-412e-870a-c02b4a6dcd0f 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.
MyTodoApp
directory.launch.json
. To open it, press Command+Shift+P
and then choose Debug: Open launch.json
.launch.json
file with the following snippet:{ "version": "0.2.0", "configurations": [ { "name": "Docker: Attach to Node", "type": "node", "request": "attach", "port": 9229, "address": "localhost", "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/app", "protocol": "inspector", "skipFiles": [ "${workspaceFolder}/node_modules/**/*.js", "<node_internals>/**/*.js" ] } ] }
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.
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:Shift+Command+D
to switch to the Debug
view. Then, click the Debug and Run
button: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
):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.
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 :if (typeof (req.session.todolist) == 'undefined') { req.session.todolist = [] }
Make sure you paste this snippet just above the line of code that calls the next
function. Your .use
function should look like below:
app.use(session({ secret: process.env.SECRET })) .use(function (req, res, next) { if (typeof (req.session.todolist) == 'undefined') { req.session.todolist = [] } next() })
id
of your running container :docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 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
docker kill
command followed by its id
:docker kill cb9f175f7af3
cb9f175f7af3
docker build
command again:docker build -t prospectone/my-todo-list .
Sending build context to Docker daemon 26.11kB Step 1/7 : FROM node:10 ---> c5d0d6dc0b5b Step 2/7 : WORKDIR /usr/src/app ---> Using cache ---> 508b797a892e Step 3/7 : COPY package*.json ./ ---> Using cache ---> c5ac875da76b Step 4/7 : RUN npm install ---> Using cache ---> 29e7b3bac403 Step 5/7 : COPY . . ---> b92f577afd57 Step 6/7 : EXPOSE 3000 ---> Running in 78606a3c2e03 Removing intermediate container 78606a3c2e03 ---> 59c2ed552549 Step 7/7 : CMD [ "npm", "run", "start-debug" ] ---> Running in e0313973bb5a Removing intermediate container e0313973bb5a ---> 70a675646c0d Successfully built 70a675646c0d Successfully tagged prospectone/my-todo-list:latest
docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret222212 -d prospectone/my-todo-list
f75d4ef8b702df13749b10615f3945ea61b36571b0dc42b76f50b3c99e14f4c6
docker logs 10f467dbb476
f75d4ef8b702df13749b10615f3945ea61b36571b0dc42b76f50b3c99e14f4c6
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 blog post, we’ll walk you through the process of dockerizing an existing application.