What is debugger?
For any developer, debugger is the best friend. One can easily find bugs in a software with a debugger.
One can add a breakpoint to pause execution. Secondly, one can also add logic to a breakpoint to halt the execution. As an example, consider a for loop having a 1000 iterations. The execution should stop when the iteration count reaches above 100. To do so, put a breakpoint on the for loop. Next, add the logic to halt the execution when the iteration goes above 100.
Besides halting a program, debuggers show memory allocations. For example, halting the execution will show memory consumed at any given point.
What is a remote debugger?
Debugging is usually done on a localhost. Doing it remotely is called remote debugging :). i.e. If you debug a software running on remote host its called remote debugging. It is helpful for multiple reasons.
For one, one can debug a software locally. Consider a scenario where software is on the cloud. It might be deployed either for dev, uat or production. Now an issue happens on the cloud but not on the localhost. In this case, it would be very helpful to connect to the cloud and attach the debugger to the process. One can execute the software line by line to evaluate the issue and fix it.
Secondly, remote debugging is also useful when the software is running inside a container. Let’s say a project is running inside Docker. One won’t be directly able to run the project and connect to it via debugger. Instead, the docker container should expose its container port. Secondly, connect the project inside docker container by configuring the remote debugger.
Docker helps in creating portable containers which are fast & easy to deploy on various machines. These containers can be run locally on your Windows, Mac & Linux. Also major cloud systems like AWS or Azure do support them out of the box. If you want to learn more Docker basics and need a cheat sheet for Docker CLI, here is an introductory article about it.
In this article, we will setup a NodeJS project to run inside a docker container. We will also setup a remote debugging for the project.
Setting up the project
Prerequisites
Before we move further, the system should have docker desktop & VS Code installed. Other than that no other requirements are there.
For the hasty ones I have made the source code available as a repository. You can check it out here.
Creating Project Files
We are going to create a very simple express Node JS project. It will simply return a static json string on opening a specific url. For this we will create a file named server.js which is the entry point to our project.
Create a server.js file with below contents:
const server = require("express")();
server.listen(3000, async () => { });
server.get("/node-app", async (_, response) => {
response.json({ "node": "app" });
});
The server.js file states that display {“node”: “app”} on opening http://localhost:3000/node-app URL in the browser.
Secondly, we will need a package.json file to configure the project and add dependencies. For that, create a package.json file with the below content.
{
"name": "node-app",
"dependencies": {
"express": "^4.17.1"
}
}
Running as Docker Container
A Dockerfile is needed to run the project as docker container. Create a Dockerfile with below contents:
# Download the slim version of node
FROM node:17-slim
# Needed for monitoring any file changes
RUN npm install -g nodemon
# Set the work directory to app folder.
# We will be copying our code here
WORKDIR /node
#Copy all files from current directory to the container
COPY . .
# Install the dependencies in the container
RUN npm install
# Run the node server with server.js file
CMD ["node", "server.js"]
# Expose the service over PORT 3000
EXPOSE 3000
Here, the project is setup to run as a simple node server without allowing any breakpoints. The container will be running the project out of a node directory inside the container. nodemon is installed globally in the container. It’s needed for watching any file change in the directory. It is explained in detail below.
Before we can run the project we still have a couple of steps to do.
Docker Compose
Docker Compose is a really helpful way to build & run docker containers with a single command. It is also helpful for running multiple containers at the same time. It is one of the reason we use docker compose instead of plain docker. To know more about docker compose and how to run multiple containers please visit the article Run Multiple Containers With Docker Compose.
Now let’s create a docker-compose.yml file to add some more configurations. Add the below contents to docker-compose.yml file once created:
version: '3.4'
services:
node-app:
# 1. build the current directory
build: .
# 2. Run the project using nodemon, for monitoring file changes
# Run the debugger on 9229 port
command: nodemon --inspect=0.0.0.0:9229 /node/server.js 3000
volumes:
# 3. Bind the current directory on local machine with /node inside the container.
- .:/node
- /node/node_modules
ports:
# 4. map the 3000 and 9229 ports of container and host
- "3000:3000"
- "9229:9229"
The docker-compose.yml file is explained point-wise below.
Point to our current directory for building the project.
Run the project using nodemon, since if there are any changes in the local directory we want to restart the project in the docker with the changes. Nodemon is a utility that will monitor for any changes in your source and automatically restart your server.
Bind our current directory to the /node directory using volumes.
In addition to exposing and binding the 3000 port for the server, expose the 9229 for attaching the debugger.
Before we can run the project we still need to configure debugger to connect to the container.
Configure Debugger
Firstly, check if you have launch.json file created in your project. launch.json defines different types configuration we can run for debugging. If it is not created visit the RUN AND DEBUG tab on the left in your VS Code as seen in the image below.
Click on the text that says create a launch.json file. Before you can proceed it will ask the type of application to proceed. Select Node.js. It will create a new launch.json file in your project with a default Node.js configuration added.
Since we are not going to run the node application locally, go ahead and delete that configuration. Instead, replace it with the whole content of launch.json with below content.
{
"version": "0.2.0",
"configurations": [
{
// 1. Type of application to attach to
"type": "node",
// 2. Type of request. In this case 'attach'
"request": "attach",
// 3. Restart the debugger whenever it gets disconnected
"restart": true,
// 4. Port to connect to
"port": 9229,
// 5. Name of the configuration
"name": "Docker: Attach to Node",
// 6. Connect to /node directory of docker
"remoteRoot": "/node"
}
]
}
The configuration added is pretty self explanatory. Basically, we are asking the debugger to connect to a remote host with port number 9229. We are also requesting the debugger to restart whenever it gets disconnected to the host. By default the debugger tries to connect on http://localhost:9229/. But project is hosted inside the /node directory in docker. To map /node remoteRoot attribute is used.
Running the project
That’s about it! Now if you run docker compose up your project will start running. For the first run it will download some layers of the node slim sdk and then install nodemon inside the docker container. But, subsequent runs would be much faster. Running docker compose up will show the below output in your terminal.
In order to attach the debugger run the Docker: Attach to Node task from the RUN AND DEBUG tab. The debugger will now attach to the /node directory of your docker container. Now, put a breakpoint on line 4 of your server.js file i.e. response.json({ “super”: “app1” });. Next, open your browser and hit http://localhost:3000. The breakpoint will be hit and the execution halted.
Source Code
Here is the link to the final source code of the project we have created.
Conclusion
Debugging is one of the best thing for development. It’s cherry on top when we are able to debug remotely. Remote debugging enables us to connect to code running not only on the cloud but also to a docker container running locally.
Hope you have enjoyed this article. Feel free to check out some of my other articles:
Comments