Success StoriesBlogContact
docker logo a blue whale carrying different containers on his back
Tutorials |

How to debug a Node.js server written in TypeScript running in Docker

Simon Abbt

February 1, 2019

TypeScript is cool, Node.js is great and Docker is also quite nice. Therefore, writing your server with Node.js in TypeScript and letting it run in Docker is awesome! Not so awesome is getting a nice debug experience running with this setup.

While TypeScript and Docker itself can improve the development experience by a fair margin, they also require more setup to get everything running.

If you now combine these technologies, you have two major layers between your source code and the code that will be executed: The compile step and the different file location inside your Docker container.

How do you debug your code in a constellation like this?

All hail the mighty console.log, right?

While it is an option which will work without any config and certainly can offer some insights, a setup where you can set breakpoints and step through and inspect your code is necessary at some point. So no **console.log**s, at least not for everything. But how do you get working breakpoints with all these abstractions?

I’ll show you a setup which we are currently using in a project and point out the main points leading to a working setup. The editor we’ll configure to debug the code is VS Code. Besides VS Code, TypeScript, Node.js and Docker, we’ll use Gulp and Nodemon to achieve a nice workflow and debug experience.

A high-level flow looks like this:

  • You write some TypeScript code
  • Gulp detects file changes and triggers a TypeScript compilation
  • The compiled JavaScript is provided to the Docker container through a mounted volume
  • A Nodemon process in the container detects file changes and restarts the Node.js server inside the container. It also exposes a debug port.

The folder structure which all of the following example configs are based on looks like this:

1
2
3
4
5
6
7
8
9
/root
  /app
    /server
      /src
      /(dist)
      docker-compose.yml
      Dockerfile
      gulpfile.js
      package.json

The configurations files for the described setup

1
2
3
4
5
6
7
8
9
10
11
version: "3"

services:
  server:
    command: npm run debug
    build: .
    volumes:
      - ./dist/src:/server/dist/src/ # same folder structure on local machine and docker container
    ports:
      - "3001:3001" # open port for server api
      - "9222:9222" # open port for inspector to debug

docker-compose.yml

1
2
3
4
5
6
FROM node:10.8.0-alpine
WORKDIR /server
COPY package.json package-lock.json ./
RUN npm ci
COPY . /server
CMD [ "npm", "run", "start" ]

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const gulp = require("gulp");
const ts = require("gulp-TypeScript");
const sourcemaps = require("gulp-sourcemaps");
const filter = require("gulp-filter");
const watch = require("gulp-watch");
const rimraf = require("rimraf");

const tsProject = ts.createProject("./tsconfig.json");

gulp.task("transpile", () => {
  const tsResult = tsProject
    .src()
    .pipe(sourcemaps.init())
    .pipe(tsProject(ts.reporter.fullReporter()))
    .on("error", () => null);

  return tsResult.js.pipe(sourcemaps.write("./")).pipe(gulp.dest("./dist/src"));
});

gulp.task("watch-server", ["transpile"], function() {
  return watch(["src/**/*.{ts,tsx}"], function() {
    gulp.start("transpile");
  });
});

gulp.task("initial-clean", function(done) {
  rimraf("./dist", done);
});

gulp.task("start-watch", ["initial-clean"], () => {
  gulp.start("watch-server");
});

gulp.task("watch", ["start-watch"]);

gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "types": ["@types/node", "node"],
    "typeRoots": ["./node_modules/@types"],
    "lib": ["es5", "es6", "es2015"],
    "rootDir": "src",
    "outDir": "dist", // specify the directory for your compiled code
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}
view raw

tsconfig.json

In your package.json, you should add a debug script:

1
"debug": "nodemon --legacy-watch --watch ./dist/src/ --inspect=0.0.0.0:9222 --nolazy ./dist/src/server.js",

Note: You have to use —-legacy-watch to detect file changes inside a Docker container.

Additionally, we need a VS Code launch configuration. Luckily, there is already a default one for docker-node we can build on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Docker: Attach to Node",
      "port": 9229,
      "address": "localhost",
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/usr/src/app",
      "protocol": "inspector"
    }
  ]
}

launch.json

With docker-compose up you should get your server running. The configured setup should give you a nice workflow which automatically compiles and restarts your server when you change the source code. But if you now set some breakpoints in VS Code, your breakpoints will look like this:

screenshot of code with an unverified breakpoint

Why won’t senpai notice me ☹️— your breakpoint

So, what is necessary to fix that? There are two main parts we have to adjust to get our breakpoints in VS Code working.

  • The debug launch config in VS Code
  • A possibly necessary source map rewrite depending on your source, dist and container workspace folder locations

A working VS Code config for our structure and settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Server in Docker",
      "port": 9222,
      "timeout": 10000,
      "stopOnEntry": true,
      "cwd": "${workspaceFolder}/app/server", // the root where everything is based on
      "localRoot": "${workspaceFolder}/app/server", // root of all server files
      "remoteRoot": "/server", // workspace path which was set in the dockerfile
      "outFiles": ["${workspaceFolder}/app/server/dist/src/**/*.js"], // all compiled JavaScript files
      "sourceMaps": true,
      "internalConsoleOptions": "neverOpen"
    }
  ]
}

The launch.json configuration file.

Note: The ${workspaceFolder} is the project folder that has been opened with VS Code. The localRoot\, ***remoteRoot\*** and ***outFiles\*** paths have to be adjusted to resemble your project structure and container settings.

In our example, we also need to rewrite the source maps because the TypeScript files aren’t located next to the compiled output files. To map them back to the original source, we need to adjust the path and go up two folders.

1
2
3
4
5
6
7
8
return tsResult.js
  .pipe(
    sourcemaps.mapSources(function(sourcePath, file) {
      return "../../" + sourcePath; // rewrite sourcePath to point to the correct TypeScript file paths
    })
  )
  .pipe(sourcemaps.write("./"))
  .pipe(gulp.dest("./dist/src"));

Changes in gulpfile.js

Debugging the debugger 😮

Setting "trace": "verbose" in your launch.json can help solving problems with the debugger. This will log all kind of config data in the debug console and can help you find some wrongly set paths. The sad thing, though: it only gives you all the configured paths if everything is working correctly.

E.g. in a working version the output would look like this:

1
2
Mapped localToRemote: /projects/ts-node-in-docker/app/server/dist/src/File.js -> /server/dist/src/File.js
SourceMaps.setBP: Mapped /projects/ts-node-in-docker/app/server/src/File.ts to /projects/ts-node-in-docker/app/server/dist/src/File.js
  • The remote path in the container is correct and maps to a valid path in the dist folder on my local machine.
  • The path from the dist folder on my machine is mapped to the correct TypeScript files.

If all paths fit, you can just set breakpoints in your TypeScript code and VS Code will trigger them: Awesome! 🎉 🎉 🎉

But if they aren’t set up correctly, you’ll only receive an output like this:

1
2
3
4
From client: setBreakpoints({“source”:{“name”:”File.ts”,”path”:”/projects/ts-node-in-docker/app/server/src/File.ts”,”sources”:[],”checksums”:[]},”breakpoints”:[{“line”:233}],”lines”:[233],”sourceModified”:false})
To client: {“seq”:0,”type”:”event”,”event”:”output”,”body”:{“category”:”telemetry”,”output”:”setBreakpointsRequest”,”data”:{“Versions.DebugAdapterCore”:”6.7.7",”Versions.DebugAdapter”:”1.27.1",”Versions.Target.Version”:”v10.8.0",”fileExt”:”.ts”}}}
To client: {“seq”:0,”type”:”response”,”request_seq”:13,”command”:”setBreakpoints”,”success”:true,”body”:{“breakpoints”:[{“verified”:false,”line”:233,”message”:”Breakpoint ignored because generated code not found (source map problem?).”,”id”:1005}]}}
To client: {“seq”:0,”type”:”event”,”event”:”output”,”body”:{“category”:”telemetry”,”output”:”ClientRequest/setBreakpoints”,”data”:{“Versions.DebugAdapterCore”:”6.7.7",”Versions.DebugAdapter”:”1.27.1",”Versions.Target.Version”:”v10.8.0",”successful”:”true”,”timeTakenInMilliseconds”:”0.718044",”requestType”:”request”}}}

That’s a lot of output 😩…

I’ve highlighted the important parts for you:

  • setBreakpoints({"source": {"name": "File.ts", "path": "/projects/ts-node-in-docker/app/server/src/File.ts”
  • "message": "Breakpoint ignored because generated code not found (source map problem?)."

That would, for example, show us that the source map paths in the compiled code are wrong — this is what we’ve already fixed with the Gulp source map rewrite.

Only if the Source Map mappings between the local compiled files and your source files match together, the debugger will try to map the compiled local files to the ones running in the Docker container.

So, fix the source map setup on your local machine first. When it is correct and you can set breakpoints, you can adjust the settings to map the compiled files to the ones in the container.

To summarize:

  • Use "trace": "verbose" to understand problems with your breakpoints and your launch.json
  • In your launch.json:
  • "cwd" = your VS Code project directory
  • "localRoot" = where your TypeScript source files are located
  • "remoteRoot" = workspace folder as configured in your Dockerfile
  • "outFiles" = glob pattern where the compiled JavaScript files are written to on your local machine
  • If necessary, rewrite source paths so that VS Code can find the source files for the compiled ones
  • Open port 9222 in Docker
  • Start node server with --inspect=0.0.0.0:9222

JavaScript

TypeScript

Docker

Nodejs

Debugging

Get in touch with us. From simple questions to complex queries about your next project, we’re happy to chat.

+49 821 / 907 80 86 0hello@peerigon.com

Your contacts:
 Michael Jaser and Johannes Ewald, Peerigon Co-founders

Wir sind Peerigon, eine Agentur für Softwareentwicklung.

Peerigon GmbH
Werner-von-Siemens-Straße 6
86159 Augsburg
+49 821 907 80 86 0

mail peerigon

service

Full-stack ConsultingSoftware DevelopmentProgramming WorkshopsTeam Support
BlogSuccess StoriesContactgo digital funding

© 2021 Peerigon

Privacy PolicyLegal NoticePress