The best way to debug a Node.js app running in a Docker container
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:
- Updating
package.json
with a debug script - Creating a debug version of our
docker-compose.yml
file - Attaching a debugger to the container
- 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
.
You should see some JSON added to your launch.json
file. Modify the file as follows:
- Set the
address
property to “localhost” as we have mapped port9229
from our container to our host machine - Set the
remoteRoot
property to/usr/src/app
as this is our Dockerfile’sWORKDIR
and contains all our source code (inside the container) - Set a new property called
restart
so that when we make a code change andnodemon
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.
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:
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.