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.
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:
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
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:
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.
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
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
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:
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.
JavaScript
TypeScript
Docker
Nodejs
Debugging
Irena, 05/23/2023
Donations
Diversity
CSD Augsburg
Queer Community
Company Culture
Go to Blogarticle
Moritz Jacobs, 03/23/2023
open source licenses
unit testing
node_modules
Go to Blogarticle
Klara, 03/08/2023
Diversity
Women in Tech
Inclusion
Company Culture
Go to Blogarticle
Peerigon: the company for bespoke software development.
service
workshop
Do you already know Konsens, our free online tool?