在容器内调试Node.js
当向Node.js项目添加Docker文件时,会添加任务和启动配置以启用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
}
}
]
}
配置应用程序“准备就绪”时
扩展确定应用程序“准备”好接收HTTP连接时,它会向调试控制台写入形式为Listening on port
的消息,就像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);
}
调试启动配置中的相应pattern
(在launch.json
中)是:
{
"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格式字符串,其中包含一个字符串标记,指示应替换端口的位置。
调试启动配置中对应的 uriFormat
(在 launch.json
中)用于打开 about.html
页面而不是主页的格式为:
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"dockerServerReadyAction": {
"uriFormat": "http://localhost:%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"
}
}
]
}
故障排除
由于缺少node_modules,Docker镜像无法构建或启动
Dockerfiles 通常以优化镜像构建时间、镜像大小或两者兼顾的方式进行安排。然而,并非每个 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"]