This recipe shows how to run and debug a VS Code TypeScript project in a Docker container.
The recipe assumes that you have a recent version of Docker installed.
You can either follow the manual steps in the next section or you can 'clone' the setup from a repository:
git clone https://github.com/weinand/vscode-recipes.git
cd vscode-recipes/Docker-TypeScript
npm install
code .
Inside VS Code press 'F5' to start the debug session. Then open a browser on localhost:3000 and watch the request counter increment every 3 seconds.
To learn what's going on, please read the following detailed explanation.
Create a new project folder 'server' and open it in VS Code. Inside the project create a folder src
with a file index.ts
:
import * as http from 'http';
let reqCnt = 1;
http.createServer((req, res) => {
const message = `Request Count: ${reqCnt}`;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<html><head><meta http-equiv="refresh" content="2"></head><body>${message}</body></html>`);
console.log("handled request: " + reqCnt++);
}).listen(3000);
console.log('server running on port 3000');
This is a trivial http server that serves a self-refreshing page showing a request counter.
Add a tsconfig.json
file in the src
folder that tells the TypeScript compiler that we need source maps and where to put them:
{
"compilerOptions": {
"outDir": "../dist",
"sourceMap": true
}
}
Add this package.json
which lists the dependencies and defines some scripts for building and running the server:
{
"name": "server",
"version": "0.0.0",
"scripts": {
"postinstall": "tsc -p ./src",
"watch": "tsc -w -p ./src",
"debug": "nodemon --watch ./dist --inspect=0.0.0.0:9222 --nolazy ./dist/index.js",
"docker-debug": "docker-compose up",
"start": "node ./dist/index.js"
},
"devDependencies": {
"@types/node": "^6.0.50",
"typescript": "^2.3.2",
"nodemon": "^1.11.0"
},
"main": "./dist/index.js"
}
- The
postinstall
script uses the TypeScript compiler to translate the source into JavaScript in the 'dist' folder. - The
watch
script runs the TypeScript compiler in 'watch' mode: whenever the TypeScript source is modified, it is transpiled into the 'dist' folder. - The
debug
script uses 'nodemon' to watch for changes in the 'dist' folder and restart the node runtime in debug mode. - The
docker-debug
script creates a docker image for debugging. - The
start
script runs the server in production mode.
You can now run the server locally with these steps:
npm install
npm start
Then open a browser on localhost:3000 and watch the request counter increment every 3 seconds.
For running the server in a docker container we need a Dockerfile
in the root of your project:
FROM node:8-slim
WORKDIR /server
COPY . /server
RUN npm install
EXPOSE 3000
CMD [ "npm", "start" ]
This creates the docker image from a node runtime image, copies the VS Code project into a /server
folder and runs npm install
to load the required npm modules and the 'build' script to build the server. Finally it starts the server via the npm 'start' script.
You can build and run a docker image 'server' with these steps:
docker build -t server .
docker run -p 3000:3000 server
For debugging this server in the Docker container we could just make node's debug port 9222 available (via the '-p' flag from above) and attach VS Code to this.
But for a faster edit/compile/debug cycle we will use a more sophisticated approach by mounting the 'dist' folder of the VS Code workspace directly into the container running in Docker. Inside Docker we'll use 'nodemon' for tracking changes in the 'dist' folder and restart the node runtime automatically and in the VS Code workspace we'll use a watch task that automatically transpiles modified TypeScript source into the 'dist' folder.
Let's start with the 'watch' task by creating a tasks.json
inside the .vscode
folder:
{
"version": "0.1.0",
"tasks": [
{
"taskName": "tsc-watch",
"command": "npm",
"args": [ "run", "watch" ],
"isShellCommand": true,
"isBackground": true,
"isBuildCommand": true,
"problemMatcher": "$tsc-watch",
"showOutput": "always"
}
]
}
The tsc-watch
task runs the npm watch
script and registers as VS Code's 'build' command.
The build command will be automatically triggered whenever a debug session is started (but it can be triggered manually as well).
For the modified Docker setup we use 'docker-compose' because it allows to override individual steps in the 'Dockerfile'.
Create a docker-compose.yml
file side-by-side to the Dockerfile
:
version: "2"
services:
web:
build: .
command: npm run debug
volumes:
- ./dist:/server/dist
ports:
- "3000:3000"
- "9222:9222"
Here we mount the dist
folder of the workspace into the Docker container (which hides whatever was in that location before). And we replace the npm start
command from CMD in the Dockerfile by npm run debug
. In the ports section we add a mapping for the node.js debug port.
The docker-compose.yml
will be used when running the docker-compose from the command line:
docker-compose up
For attaching the VS Code node debugger to the server running in the Docker container we use this launch configuration:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to Docker",
"preLaunchTask": "tsc-watch",
"port": 9222,
"restart": true,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/server",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}
- As a
preLaunchTask
we run the watch task that transpiles TypeScript into thedist
folder. Since this task runs in background, the debugger does not wait for its termination. - The
localRoot
/remoteRoot
attributes are used to map file paths between the docker container and the local system:remoteRoot
is set to/server
because that's the absolute path of the folder where the program lives in the docker container. - The
restart
flag is set totrue
because VS Code should try to re-attach to node.js whenever it loses the connection to it. This typically happens when nodemon detects a file change and restarts node.js.
After running "Attach to Docker" you can debug the server in TypeScript source:
- set a breakpoint in
index.ts:9
and it will be hit as soon as the browser requests a new page, - modify the message string in
index.ts:7
and after you have saved the file, the server running in Docker restarts and the browser shows the modified page.
Please note: when using Docker on Windows, modifying the source does not make nodemon restart node.js. On Windows nodemon cannot pick-up file changes from the mounted
dist
folder because of this issue. The workaround is to add the--legacy-watch
flag to nodemon in thedebug
npm script:
"debug": "nodemon --legacy-watch --watch ./dist --inspect=0.0.0.0:9222 --nolazy ./dist/index.js",
Please note: the following requires VS Code 1.13.0
Instead of launching Docker from the command line and then attaching the debugger to it, we can combine both steps in one launch configuration:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch in Docker",
"preLaunchTask": "tsc-watch",
"runtimeExecutable": "npm",
"runtimeArgs": [ "run", "docker-debug" ],
"port": 9222,
"restart": true,
"timeout": 60000,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/server",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
- here we set the
docker-debug
npm script as the runtime executable and its arguments. The node debugger doesn't care about what is used as theruntimeExecutable
as long as it opens a debug port that the node debugger can attach to. - we use the
integratedTerminal
because we want to be able to kill docker-compose by using 'Control-C'. This is not possible with the Debug Console.