Debugging NodeJS in Docker

Describing how I debug a local NodeJS & TypeScript service when developing

Posted by Eddo on June 16, 2020

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

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.