在容器内调试 Node.js
在将 Docker 文件添加到 Node.js 项目时,将添加任务和启动配置以支持在 Docker 容器内调试该应用程序。但是,由于围绕 Node.js 的庞大生态系统,这些任务无法适应每个应用程序框架或库,这意味着某些应用程序需要额外的配置。
配置 Docker 容器入口点
Docker 扩展通过 package.json
的属性推断 Docker 容器的入口点,即在 Docker 容器内以调试模式启动应用程序的命令行。扩展首先查找 scripts
对象中的 start
脚本;如果找到,并且它以 node
或 nodejs
命令开头,则使用它来构建以调试模式启动应用程序的命令行。如果未找到或它不是已识别的 node
命令,则使用 package.json
中的 main
属性。如果两者都未找到或未识别,则需要显式设置用于启动 Docker 容器的 docker-run
任务的 dockerRun.command
属性。
一些 Node.js 应用程序框架包括用于管理应用程序的 CLI,并用于在 start
脚本中启动应用程序,这会掩盖底层的 node
命令。在这种情况下,Docker 扩展无法推断启动命令,您必须显式配置启动命令。
示例:配置 Nest.js 应用程序的入口点
{
"tasks": [
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": ["docker-build"],
"dockerRun": {
"command": "nest start --debug 0.0.0.0:9229"
},
"node": {
"enableDebugging": true
}
}
]
}
示例:配置 Meteor 应用程序的入口点
{
"tasks": [
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": ["docker-build"],
"dockerRun": {
"command": "node --inspect=0.0.0.0:9229 main.js"
},
"node": {
"enableDebugging": true
}
}
]
}
自动将浏览器启动到应用程序的入口页面
Docker 扩展可以在应用程序在调试器中启动后自动将浏览器启动到应用程序的入口点。此功能默认启用,并通过 launch.json
中的调试配置的 dockerServerReadyAction
对象进行配置。
此功能依赖于应用程序的几个方面
- 应用程序必须将日志输出到调试控制台。
- 应用程序必须记录“服务器已准备就绪”消息。
- 应用程序必须提供可浏览的页面。
虽然默认设置可能适用于基于 Express.js 的应用程序,但其他 Node.js 框架可能需要显式配置这些方面之一或多个。
确保应用程序日志写入调试控制台
此功能依赖于应用程序将其日志写入附加的调试器的调试控制台。但是,并非所有日志记录框架都写入调试控制台,即使在配置为使用基于控制台的记录器时也是如此(因为一些“控制台”记录器实际上会绕过控制台,并直接写入 stdout
)。
解决方案因日志记录框架而异,但通常需要创建/添加一个确实写入控制台的记录器。
示例:配置 Express 应用程序以写入调试控制台
默认情况下,Express.js 使用 debug 日志记录模块,该模块可能会绕过控制台。这可以通过显式将日志函数绑定到控制台的 debug()
方法来解决。
var app = require('../app');
var debug = require('debug')('my-express-app:server');
var http = require('http');
// Force logging to the debug console.
debug.log = console.debug.bind(console);
还要注意,debug
记录器仅在通过 DEBUG
环境变量启用时才写入日志,该变量可以在 docker-run
任务中设置。(当 Docker 文件添加到应用程序时,此环境变量默认设置为 *
。)
{
"tasks": [
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": ["docker-build"],
"dockerRun": {
"env": {
"DEBUG": "*"
}
},
"node": {
"enableDebugging": true
}
}
]
}
配置应用程序何时“准备就绪”
当应用程序将格式为 Listening on port <number>
的消息写入调试控制台时,扩展会确定应用程序已“准备就绪”以接收 HTTP 连接,如 Express.js 默认情况下所做的那样。如果应用程序记录了不同的消息,则应将调试启动配置的 dockerServerReadyAction 对象的 pattern
属性设置为与该消息匹配的 JavaScript 正则表达式。正则表达式应包含与应用程序正在侦听的端口相对应的捕获组。
例如,假设应用程序记录了以下消息
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Application has started on ' + bind);
}
调试启动配置(在 launch.json
中)中的相应 pattern
是
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"dockerServerReadyAction": {
"pattern": "Application has started on port (\\d+)"
}
}
]
}
注意端口号的
(\\d+)
捕获组,以及在\d
字符类中使用\
作为 JSON 转义字符来转义反斜杠。
配置应用程序入口页面
默认情况下,Docker 扩展将打开浏览器的“主”页面(无论该页面如何由应用程序确定)。如果浏览器应打开到特定页面,则调试启动配置的 dockerServerReadyAction 对象的 uriFormat
属性应设置为 Node.js 格式字符串,其中包含一个字符串标记,指示应在何处替换端口。
调试启动配置(在 launch.json
中)中的相应 uriFormat
以打开 about.html
页面而不是主页面将是
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"dockerServerReadyAction": {
"uriFormat": "https://127.0.0.1:%s/about.html"
}
}
]
}
将 Docker 容器源文件映射到本地工作区
默认情况下,Docker 扩展假设运行的 Docker 容器中的应用程序源文件位于 /usr/src/app
文件夹中,然后调试器将这些文件映射回打开的工作区的根目录,以将容器中的断点翻译回 Visual Studio Code。
如果应用程序源文件位于不同的位置(例如,不同的 Node.js 框架具有不同的约定),无论是在 Docker 容器内还是在打开的工作区内,则应设置调试启动配置的 node 对象的 localRoot
和 remoteRoot
属性之一或两者,分别设置工作区和 Docker 容器中的根源位置。
例如,如果应用程序位于 /usr/my-custom-location
中,则相应的 remoteRoot
属性将是
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"node": {
"remoteRoot": "/usr/my-custom-location"
}
}
]
}
故障排除
Docker 映像因缺少 node_modules 而无法构建或启动
Dockerfile 通常以优化映像构建时间、映像大小或两者的方式进行排列。但是,并非每个 Node.js 应用程序框架都支持所有典型的 Node.js Dockerfile 优化。特别是,对于某些框架,node_modules
文件夹必须是应用程序根文件夹的直接子文件夹,而 Docker 扩展会构建一个 Dockerfile,其中 node_modules
文件夹位于父级或祖先级别(这通常被 Node.js 允许)。
解决方案是从 Dockerfile
中删除该优化
FROM node:lts-alpine
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
# Remove the `&& mv node_modules ../` from the RUN command:
# RUN npm install --production --silent && mv node_modules ../
RUN npm install --production --silent
COPY . .
EXPOSE 3000
RUN chown -R node /usr/src/app
USER node
CMD ["npm", "start"]