There are some things that should be installed before we get started:
- Node JS
- Visual Studio Code (a.k.a VS Code)
- Host - this is your computer where you are working. In computer networking terms (roughly saying), it is a computer that communicates with other computers.
- Docker image - the set of layers/instructions you describe to run (it is more like a sequence, where the order of commands matter).
- Docker container - instance of your image. Roughly saying, it is like an instance of some “class” (OOP).
Clone this project and let’s get started!
It all starts with npm init
Imagine that you are working on a computer where Node.js is not installed. One way to proceed with initialization of your project is to install Node.js locally using your package manager (e.g. apt) and then proceed with
npm init. However, there is a cooler, more portable, and more cross-platform way of doing this; where, no version conflicts occur, no manual explicit configurations are needed to be set up with changing OS environment, etc. In other words,
Docker is a container management service. The keywords of Docker are develop, ship and run anywhere. The whole idea of Docker is for developers to easily develop applications, ship them into containers which can then be deployed anywhere. What a brilliant and lovely idea for the DevOps workflow.
I assume you have latest Docker installed. Docker uses images (check the definition above) to run containers which are, roughly saying, isolated processes that share the same OS kernel. Note that that Docker containers are NOT magical, lightweight VMs! If you are interested how Docker Containers work behind the scenes, take a look at the following talk given by Jérôme Petazzoni at DockerCon EU.
Let’s initialize our project by using the latest Node.js image. The following command runs an interactive bash terminal, which lets us access the container with Node.js installed in it, and binds a current directory of the host to the
/app directory in the container, which lets us persist our files (e.g. package.json, etc.).
You will immediately notice that your terminal’s hostname has changed to something like
root@b95028b5a79c:/#. Congrats! Now, you are in a container with Node.js present inside! How cool is that, huh? :)
Now, in order to access our files and initialize the project, open the
/app folder and run
You can also run
ls command to see what’s in the current directory. You will see that you have everything there from the current directory of the host.
exit to exit from the container. Since you have attached a volume using a
-v option when running a container, your package.json could be seen in your current directory in the host.
Congratulations! So far, you have managed to initialize a Node.js project using Docker (images and running containers) without actually installing Node.js in your computer. It is quite lovely, isn’t it?
Dockerfile and docker-compose
If your app uses MongoDB as a database service, you already have 2 services interacting (i.e. API <-> DB). If you want to dockerize your multi-service application, then you have to define 2 images for those services using Dockerfiles (one per service).
Docker-compose is another Docker tool which lets us manage multi-container applications. With Docker-compose, it is simpler to manage and scale your services. Docker-compose works almost the same way as the
docker command; instead of providing a Dockerfile, you can configure your services by creating the
docker-compose.yml file. Though in a bit different way, you can configure the same way as in a Dockerfile: mounting volumes, running commands, pulling images, etc.
For instance, take a look at the image above. We can define a single Dockerfile for the API, and define the configs for Mongo just inside the Compose file. Why don’t we create Dockerfiles for it? Indeed, you could do so and configure your DB (e.g. create users, roles, priviliges, etc. upon initialization). However, in this case, we don’t need to configure anything, we just need the MongoDB service running in the respective container.
NOTE: We would use Dockerfile for the Node.js API service because we have to build the app first, i.e. install its dependencies, transpile (if we use Typescript, etc.). This is how Dockerfile for the Node.js would look like:
So, what have we done? We wrote a sequence of instructions which defines your image. We pulled Node (version 9) from the Docker Hub, created a directory
/app inside the container, we “told” Docker to work with the
/app directory, and copied everything (i.e. package.json, src folders, readme, etc.) from the current folder of the host machine into the
/app folder in the container. And, ultimately, we ran the
npm install command inside the container, so we get the dependencies from the package.json installed.
Now, let’s create the
docker-compose.yml file where we will define set the configurations (env variables, volumes, networks, etc.) for the services in our architecture. Here is an example of how to define these:
Note that you could leverage links as well, though I personally love networks because of their simplicity: you can reach other services in the same network just by using their service name (in our case, they are
database). One could also define
working directory inside the compose file (by using
working_dir field) instead of specifying it in the Dockerfile.
For more details on Docker Compose, I would recommend the following Compose file Reference
Typescript and Node.js
.ts files are compiled to
In order to get started with Typescript, we have to install
typescript module via npm. We can do this by running our Node container again. Do not forget to attach a volume so the change in package.json is actually saved on our host:
At the same time, let’s install
express and the type for it to run the Express server! Note that I will not be using MongoDB in this case (though you should experiment and try yourself!).
As there is already some boilerplate code defined in
src/server.ts file, let’s change the package.json so that we first compile the code from TS to JS, and then run it! Make sure you are out of the container.
We also need to create a
tsconfig.json file that configures the Typescript compiler. More details on TS compilation configurations, check this link out. Here is our example:
Now, let’s create a
docker-compose.yml file with which we will run a Node.js server. Here, we will not use a Dockerfile; we will instead configure the API service straight in the Compose yaml file.
Before running the container, we have to install our dependencies. You may not have Node.js in your computer, or you may have a different version, so let’s run a Node container (of latest version) and install our dependencies (do not forget to attach a volume):
To run the actual service, type the following command (the
-f option specifies the file to run) to run the server:
Note if you are getting ERROR: If you are getting the
ERROR: Error processing tar file(exit status 1): unexpected EOF, run the following commands:
If you see
Listening on port 3000! message, yay! Open a browser and type
localhost:3000, and you will see the server is up. You can also try opening the routes, i.e.
Debugging with Visual Studio Code
Since the VS Code Node.js debugger communicates to the Node.js runtimes through wire protocols, the set of supported runtimes is determined by all runtimes supporting the wire protocols:
- legacy: the original V8 Debugger Protocol which is currently supported by older runtimes.
- inspector: the new V8 Inspector Protocol is exposed via the
--inspect flagin Node.js versions >= 6.3. It addresses most of the limitations and scalability issues of the legacy protocol.
As we are running a server from a Docker container, we have to attach a remote debugger. We need to add a launch configuration to the
.vscode folder, i.e.
launch.json. Here is an example of the launch configuration file:
We set a remote root to be path in the container, where our program lives.
Now, in order to add remote debugging, we have to add another script to package.json:
debug script will build (i.e. transpile the TS code) project and will start Node runtime in debugging mode accessible remotely on port
9229 (remember the port we specified above?).
Let’s create another Compose file, which we will use for running the server in the debug mode:
The only differences are that we are now running in debug mode and we attached extra port for debugging (9229).
Type the following command in the terminal so you can run the debug server:
And you will see the following message
api_1 | Debugger listening on ws://0.0.0.0:9229/f3cdf4f2-4685-21e6-8c31
Yay! Now, as the debugger is listening on
0.0.0.0:9229, we have to start debugging via VS Code. If you click
CTRL + SHIFT + D keys, the “debug” mode will open. You will see the “Remote Debugging” slider upper-left corner and the button (looks like green triangle). E.g.:
Be courageous and click on the green triangle button. Congratulations! You have just started debugging your app using VS Code and Docker containers!
Bonus round! Simplifying debugging further …
Launching container manually, and proceeding to Debug view on VS Code may be a bit of a daunting and annoying task. However, we can automate that and make our lives a bit easier: let’s make it so that we can start debugging by just clicking F5 key! :squirrel:
Let’s first create the tasks file inside the
.vscode directory, where our configurations for the VS Code reside. Copy-paste the following into the
Tasks help you automate building and testing your app. Whenever you start debugging, they may help you build your app first. There may be many tasks per launch configuration, so you can automate anything that annoys you and the steps you constantly repeat whenever you start debugging.
In the task above, we label it as “launch-debug-container” and make it execute a command to start the containers specified in the
Now, how do we perform a task when we actually launch our debugging? We have to adjust
launch.json by adding another field in our “Remote Debugging” configuration:
By giving a label from
tasks.json to the preLaunchTask property, our task will be executed first before launching our debugger. Note that I also change timeout to 60 seconds (default is 10 sec), as the containers
docker-compose.debug.yml take some time to start.
In addition, we want our containers to be stopped and removed after the debugging. If you don’t want to do so, then you are done! Otherwise, if you don’t like the containers still running after you have finished debugging, let’s add another task that will execute a command to stop and remove containers after the debugging session.
Add the following to the list of tasks in your
This will turn the containers down whenever you finish the debugging process. Let’s also adjust the launch configuration in the
launch.json by adding a
Now is the moment … Just click F5 and your debugging starts automagically. If you exit debugging, you will see the containers terminating. Good job!
Note: if you are getting
The specified task cannot be tracked error, click the Debug anyway button and your debugger will start.
Thank you + Feedback
Thank you and good job for reading the entire tutorial! It’s been a long markdown to read; anyway, you have just learned something new which will help you a lot debugging your Node.js apps and become a better developer!
Constructive feedback is always welcome via issues on this repo.
PS. If something does not work, or if you have any problems, please open an issue in this repo and I will do my best to help you asap.