Debug nodejs Application In Docker Container Using WebStorm - Part 2
In part 1 of this post we created a very simple "Hello, world!" example for a RESTful web service. We also demonstrated hitting a breakpoint when debugging our service. In this second part we will take it to the next level by deploying web service into a Docker container.
Note that you will find it easier to follow this post if you have some basic understanding of Docker. One option is to work through the first two parts of the official Docker tutorial which I found quite well made.
Also, this second part continues where part 1 left off. We won't repeat the steps from part one. As a reminder the source code is available on github.
Note that you will find it easier to follow this post if you have some basic understanding of Docker. One option is to work through the first two parts of the official Docker tutorial which I found quite well made.
Also, this second part continues where part 1 left off. We won't repeat the steps from part one. As a reminder the source code is available on github.
Docker
To docker-ize (or container-ize) our application we need two elements:
- A Dockerfile which describes how the image is created, i.e. the steps to create the container image.
- A docker-compose.yml file that contains parameters for creating the image
Oversimplified you could say that the docker-compose.yml file contains the ingredients while the Dockerfile contains the instructions for creating the dish from those ingredients.
Dockerfile
Let's start with the Dockerfile. By convention it's named "Dockerfile" (no extension!). Within WebStorm right-click on the folder "hello-express" and select "New File". Then enter "Dockerfile" as the name and click "OK". Then copy the following content into file "Dockerfile":
Line 1 tells Docker what image we use as the starting point for the new image. In this case we use an image named/tagged with "node:10-alpine" which is essentially a Linux based container that has nodejs preinstalled. Don't worry that it's a Linux container. That detail is essentially hidden from us apart form the fact that absolute paths start from root ("/") as with all Unix systems. Don't worry about it because Docker will map our Windows path properly to the paths in the image (i.e. inside the Linux filesystem). If you have installed Docker properly on your box and logged in to Docker, the "FROM" command will know where to get the image from if it cannot find it on your box.
Line 2 creates a new folder inside of the container at location "/home/nodejs/app". That is where our application's files will be located.
Line 3 sets the working directory in the new image to the directory we just created.
Line 4 looks a bit as if we were copying files onto themselves. However, here the source path (the first period) is the current file system while the destination path (the second period) is in the target container.
Line 5 then instructs the npm that comes with the base docker image - "node:10-alpine" - to install the packages required for production. Note that in line 4 we copied all files of our application to the new Docker image including our file "package.json". npm will use that package.json file to determine what packages to install (including their dependencies).
Finally in line 6 we invoke the command that launches our application. The first argument it the name of the program to execute - "node" - while the second argument - "hello_express.js" - is a parameter that is passed to "node".
We will later modify this file slightly to add support for debugging. Let's leave it like this for the moment, though.
Finally in line 6 we invoke the command that launches our application. The first argument it the name of the program to execute - "node" - while the second argument - "hello_express.js" - is a parameter that is passed to "node".
We will later modify this file slightly to add support for debugging. Let's leave it like this for the moment, though.
docker-compose.yml
We have the instructions for how to create the container image. The second element is the parameters used by those instructions. These parameters are provided using a file named "docker-compose.yml".
Again right-click on the "hello-express" folder within WebStorm, then select "New File" and enter "docker-compose.yml". By convention this is the default name for it.
In line 1 we specify the version of the docker-compose file. As of writing the recommended version is '3'. By the time you read this a later version may be recommended. Consult the documentation to find out.
From line 2 we specify the services our image will consist of. In this simple case we only have one service which we call "backend".
Build time directives for service "backend" are in lines 4 to 6. Line 5 provides the location of the dockerfile relative to the docker-compose file. In this case we just have "." which means the dockerfile will be in the same directory as the docker-compose file. Line 6 provides the name of the dockerfile, "Dockerfile" in this case.
In line 7 we define the image name to be "helloexpress:latest". This has to be all lowercase. "latest" by convention indicates it's the latest version. The tag can be any name but it pays off to follow the conventions.
In line 8 we define the container name. For our example we'll use "helloexpress.backend". Again, it needs to be all lowercase and you are free to choose whatever works best for you.
In lines 9 and 10 we set NODE_ENV to "development". This instructs nodejs to produce more meaningful output, not to minify javascript or CSS, not to cache any views, and other things that are meaningful during development. You don't want to use this in production, though, for performance reasons. Some more details about the differences between "development" and "production" are described in this post.
Lines 11 and 12 are interesting as well. This allows you mapping ports as seen from within the container to different ports seen from outside the container. This is useful is several scenarios. For us it's useful to use 3100 as the external port which is then mapped to 3000 within the container. That way we know whether we run our app inside the container or without Docker. This mapping is transparent. Our app will still listen on port 3000.
From line 2 we specify the services our image will consist of. In this simple case we only have one service which we call "backend".
Build time directives for service "backend" are in lines 4 to 6. Line 5 provides the location of the dockerfile relative to the docker-compose file. In this case we just have "." which means the dockerfile will be in the same directory as the docker-compose file. Line 6 provides the name of the dockerfile, "Dockerfile" in this case.
In line 7 we define the image name to be "helloexpress:latest". This has to be all lowercase. "latest" by convention indicates it's the latest version. The tag can be any name but it pays off to follow the conventions.
In line 8 we define the container name. For our example we'll use "helloexpress.backend". Again, it needs to be all lowercase and you are free to choose whatever works best for you.
In lines 9 and 10 we set NODE_ENV to "development". This instructs nodejs to produce more meaningful output, not to minify javascript or CSS, not to cache any views, and other things that are meaningful during development. You don't want to use this in production, though, for performance reasons. Some more details about the differences between "development" and "production" are described in this post.
Lines 11 and 12 are interesting as well. This allows you mapping ports as seen from within the container to different ports seen from outside the container. This is useful is several scenarios. For us it's useful to use 3100 as the external port which is then mapped to 3000 within the container. That way we know whether we run our app inside the container or without Docker. This mapping is transparent. Our app will still listen on port 3000.
Building The Image
With these two files in place we can create the Docker image using the command "docker-compose build". You can use WebStorm's Terminal window for that. The window should show a couple of success messages:
Up-ing the Container
Once we have the container we can create a container from the image and run the container. The command "docker-compose up" will perform all the steps for us:
Once we execute that command we should see the following messages:
The last message suggests that our app is now listening on port 3000. Let's confirm. Note, please ignore the swarm related message in this screenshot as it is specific to my docker environment.
To confirm we can again use our little service let use the "REST Client" once more as we did in part 1 of this post.
The request failed to connect. So what is going on? The answer is easy. Remember that we told Docker to map the (outside) port 3100 to the (inside) port 3000. This means we now need to use port 3100 instead of 3000 as we are looking at the container from the outside. Let's try again, this time with port 3100:
That's much better!
Let's recap: We now have a position where we can run our app inside of a container. By mapping port 3100 to 3000 we know that we are actually using the Docker container. The same request to port 3000 now fails.
As we are now at the point where we can add the configuration required to debug our application. We'll cover that in part 3 of this set of posts.
Comments
Post a Comment