Since a month or two I’ve switched teams, and now implementing features in NodeJS and a React app. As easy as debugging a PHP application can be, I have seen some difficulty in getting debugging running properly for a NodeJS service. With this article I’d like to elaborate on how I’ve achieved a better way than using console.log(var)
statements.
The NodeJS service I’m referring to is a single microservice that acts as part of a larger API. It is written in TypeScript, and has dependencies on other microservices. For local development it is running in Docker and I’m using the other services in the staging environment. The dependency of a database is handled by a second Docker container.
Orchestrating your local application
To create my local environment, I’ve created a docker-compose.yml
file that starts the MySQL database and the app.
version: '3.4'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_ROOT_PASSWORD: ""
MYSQL_DATABASE: nodejs-dev
restart: always
volumes:
- db-data:/var/lib/mysql
ports:
- 3306:3306
app:
build:
context: .
dockerfile: Dockerfile
target: base
environment:
DB_HOST: mysql2://root:@mysql:3306/
DB_NAME: nodejs-dev
LOG_LEVEL: debug
NODE_ENV: develop
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules # always use the container version of `node_modules` folder
ports:
- 3000:80
- 9229:9229
restart: on-failure
# Don't start npm before the MySQL db is ready
command: ./wait-for.sh mysql:3306 -- npm run debug
depends_on:
- mysql
volumes:
db-data:
driver: local
Before I joined the team, the application was already deployed as a Docker container. Initialy I created a second Dockerfile
for local development, but quickly found on Stackoverflow that as of a docker-compose
version 3.4 I could target a specific build stage to run the app locally. This negated the need of a second Dockerfile.
#
# ---- Base ----
FROM node:12 AS base
ENV HOME=/usr/src/app
WORKDIR $HOME
# 'netcat' is equired by 'wait-for.sh'
RUN apt-get -q update && apt-get -qy install netcat
COPY ./wait-for.sh $HOME/
COPY ./package.json $HOME/
COPY ./package-lock.json $HOME/
RUN npm ci --log-level=error
#
# ---- Deps ----
FROM base AS deps
RUN npm prune --only=prod --log-level=error
#
# ---- Build ----
FROM base AS build
COPY . $HOME
RUN npm run build
#
# ---- Release ----
FROM node:12-alpine
ENV HOME=/usr/src/app
WORKDIR $HOME
COPY --from=deps $HOME $HOME
COPY --from=build $HOME/build $HOME/build
EXPOSE 80
CMD ["npm", "start"]
In order to get the application running I now only have to type docker-compose up
in my command line. This will build the app for me, if not already done, and make sure I can connect to it locally. As the NodeJS app depends on the MySQL database to be running, I’m using a wait-for
script to defer starting NodeJS until the MySQL db is ready.
Debugging
To allow debugging there are a few steps needed. First thing is to ensure the TypeScript files are compiled with source maps enabled. Otherwise you need to debug the compiled files instead of the original TypeScript files. I’ve enabled that by adding a builddev
script as npm run
command in package.json
:
"scripts": {
"builddev": "./node_modules/.bin/babel src --out-dir build --extensions '.ts' --copy-files --source-maps",
"predebug": "npm run builddev && npm run migrate",
"debug": "nodemon --watch src --inspect=0.0.0.0 build/index.js"
}
The app itself is started with npm run debug
in the local Docker container which enables the use of the NodeJS inspector. The debugger of your IDE should then be able to connect to it. This allows you to step through your code upon execution.
💡Run docker-compose run --rm app npm run watch
if you want to continuously build the Typescript. The other, already running, container will take the built files and restart itself. General idea is that I don’t want to run any npm
scripts locally, only in Docker.
The second step is to configure your IDE. I’m using Visual Studio Code and have added the following launch configuration to connect to my local NodeJS app in Docker:
{
"name": "Docker: Attach to Node",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"cwd": "${workspaceFolder}",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/app",
"outFiles": [ "${workspaceRoot}/build/**/*.js" ],
"sourceMaps": true,
"protocol": "inspector"
}
I can now use breakpoints that I set in my TypeScript code when calling the local NodeJS service 💪🏻.
There is now one thing that I still don’t have fixed, that is running mocha
tests within a Docker container. I still need to run npm
locally for that to work unfortunately. Something for another day to resolve.
References
- https://medium.com/@guillaumejacquart/node-js-docker-workflow-12febcc0eed8
- https://thestartupfactory.tech/journal/how-to-fully-utilise-docker-during-development
- https://medium.com/@semur.nabiev/how-to-make-docker-compose-volumes-ignore-the-node-modules-directory-99f9ec224561
P.S. If you’ve enjoyed this article or found it helpful, please share it, or check out my other articles. I’m on Instagram and Twitter too if you’d like to follow along on my adventures and other writings, or comment on the article.