学习调试运行在Docker容器中的Node.js应用程序

465 阅读6分钟

调试运行在Docker容器中的Node.js应用程序

调试一个应用程序涉及到检测和消除代码中现有的错误,也就是所谓的bug,这些错误可能会导致它意外地工作甚至崩溃。调试允许开发人员发现并解决应用程序中的错误或缺陷,以防止不正确和不需要的操作。

在本教程中,我们将建立一个带有错误的Node.js应用程序,在Docker容器中运行,并使用Node.js的Visual Studio Code调试工具对其进行调试。本教程可供开发人员在构建其应用程序时参考。

前提条件

要跟上本教程,你需要具备以下条件。

  • 对[Docker容器]和[Node.js]有初步了解。
  • 安装了[Docker]。
  • 安装了[Node.js]。
  • 安装了[Microsoft Visual Studio Code]。
  • 一个网络浏览器。我将使用[谷歌浏览器]。

第1步:创建根目录

我们将创建一个简单的待办事项列表应用程序,允许用户添加和删除任务。在对应用程序进行编码时,我们还将引入一个小错误,并使用Visual Studio Code来处理和修复这个问题。

打开一个新的终端窗口,浏览到一个根目录,并运行下面的命令。

$ mkdir TodoApp
$ cd TodoApp

第2步:初始化Node.js项目

通过执行下面的命令来初始化该项目。

$ npm init -y

上述命令将在TodoApp 文件夹中生成一个名为package.json的文件。package.json 文件包含与Node.js项目有关的信息。它还向npm提供了关于项目依赖的信息。

该文件将看起来像这样。

packagejson

创建待办事项应用程序

接下来,我们将使用Express.js构建我们的待办事项应用程序:一个快速、流行和轻量级的Node.js网络框架。

Express使网络应用程序的开发更加容易。

第1步:安装项目的依赖项

我们将通过运行下面的命令来安装express 和应用程序所需的其他一些先决条件。

$ npm install express body-parser cookie-session ejs --save

第2步:为app.js文件编码

然后我们将在TodoApp 目录中用下面的代码创建一个名为app.js 的文件。

const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const session = require('cookie-session')
const urlencodedParser = bodyParser.urlencoded({ extended: false })
const port = 3000

app.use(session({ secret: process.env.SECRET }))
  .use(function (req, res, next) {
    next()
  })

  .get ('/todo', function (req, res) {
    res.render('todo.ejs', { todolist: req.session.todolist })
  })

  .post ('/todo/add/', urlencodedParser, function (req, res) {
    if (req.body.newtodo != '') {
      req.session.todolist.push(req.body.newtodo)
    }
    res.redirect('/todo')
  })

  .get ('/todo/delete/:id', function (req, res) {
    if (req.params.id != '') {
      req.session.todolist.splice(req.params.id, 1)
    }
    res.redirect('/todo')
  })

  .use (function (req, res, next) {
    res.redirect('/todo')
  })


  .listen(port, () => console.log(`MyTodo app is listening on port ${port}!`))

第3步:创建todo.ejs文件

接下来我们将创建一个文件./views/todo.ejs ,并在其中粘贴以下代码。

<!DOCTYPE html>

<html>
    <head>
        <title>My todolist</title>
        <style>
            a {text-decoration: none; color: black;}
        </style>
    </head>

    <body>
        <h1>My todolist</h1>

        <ul>
        <% todolist.forEach(function(todo, index) { %>
            <li><a href="/todo/delete/<%= index %>"></a> <%= todo %></li>
        <% }); %>
        </ul>

        <form action="/todo/add/" method="post">
            <p>
                <label for="newtodo">What should I do?</label>
                <input type="text" name="newtodo" id="newtodo" autofocus />
                <input type="submit" />
            </p>
        </form>
    </body>
</html>

第4步:启动网络服务器

通过执行下面的命令来启动网络服务器。

$ export SECRET=bestkeptsecret; node app.js

控制台窗口将显示一个成功信息,即服务器正在运行并监听指定端口。

创建一个Docker镜像

现在我们已经成功创建了一个Node.js应用程序。

接下来,我们将为它创建一个Docker镜像

一个Docker容器是由一个Docker镜像构建的,它包含了用Docker部署和运行应用程序所需的必要信息。

要运行一个Docker容器,我们可以创建自己的Docker镜像,或者下载一个已经建立的Docker镜像。在我们的案例中,我们将创建我们自己的Docker镜像。一个Docker镜像是由多层组成的,是一个只读文件系统。

Docker的工作方式是,它为Docker文件中包含的每条指令创建一个。每一个新层都驻扎在之前的层之上。

对于可能经常变化的应用程序的代码,建议将其放在文件的最后。

第1步:创建一个Dockerfile

在项目的根目录下创建一个名为Dockerfile的文件,其中包含以下几行代码。

FROM node:14
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "run" , "start-debug" ]

让我们仔细看看我们刚刚创建的文件。

  • FROM。设置基本镜像。以后要添加的所有东西都将基于这个镜像。在本教程中,我们将使用Node.js的14版。
  • WORKDIR:这里设置了COPYRUNCMD 命令的工作目录。
  • RUN:它在Docker容器中执行npm install 命令。
  • COPY:它将文件复制到构建环境中的Docker镜像中。
  • EXPOSE:它确保在Docker容器中运行的进程在3000端口上监听。它有助于将端口从主机转发到容器上。
  • CMD:它在我们的Docker容器启动后,在容器内执行node app.js 命令。

第2步:创建一个.dockerignore文件

由于我们不希望向构建上下文发送大文件,并加快系统的速度,我们使用.dockerignore 文件。

它是一个类似于.gitignore 的文本文件,包含了不应该被添加到构建中的目录和文件的名称。文件.dockerignore ,将包含以下内容。

node_modules
npm-debug.log

第3步:构建一个Docker镜像

接下来,我们将执行docker build 命令来构建我们的Docker镜像。

$ docker build -t to-do-app .

-t 标志指定了镜像的名称。上下文的路径指向我们要从Docker文件中引用的一组文件。

正如前面所讨论的,docker build 命令为Docker文件中的每个命令添加了一个新层。

一旦命令执行成功,Docker就会删除中间的容器。

第4步:运行Docker容器

有了我们的Docker镜像,我们现在可以通过执行docker run 命令来运行它,并把下面的参数传给它。

  • -p 是主机上的端口。在我们的例子中,我们将使用3001端口,该端口将被转发到容器的3000端口,并以 。:
  • -e 将创建一个环境变量。在我们的例子中,我们使用 ,并把它的值分配给 。SECRET bestkeptsecret
  • -d 确保容器将在后台执行。
  • 本例中图像的名称是:to-do-app

整个命令将看起来像下面这样。

$ docker run -p 3001:3000 -e SECRET=bestkeptsecret -d to-do-app

第5步:验证容器是否正在运行

我们可以用以下方法验证我们的Docker容器是否在运行。

$ docker ps

输出将类似于下图所示。

$ docker ps

CONTAINER ID   IMAGE          COMMAND                  CREATED       STATUS          PORTS                                       NAMES
9a8a8e6a13ae   a94edd9b09fd   "docker-entrypoint.s…"   2 weeks ago   Up 49 seconds   0.0.0.0:3001->3000/tcp, :::3001->3000/tcp   nostalgic_diffie

第6步:检查Docker日志

要检查日志,请执行docker logs 命令,然后输入容器的id

$ docker logs 9a8a8e6a13ae

输出将显示如下。

$ docker logs 9a8a8e6a13ae

MyTodo app is listening on port 3000!

第7步:在网络浏览器上测试应用程序

现在,我们的应用程序已经启动并运行。我们可以浏览到链接http://localhost:3001,我们尝试添加一个新的todo。正如我们在下面看到的,应用程序在todo.ejs 文件的行上显示一个错误:todolist.forEach(function(todo, index)

app-error

在下面的步骤中,我们将看看如何使用Visual Studio Code来调试上面的错误。

第8步:停止Docker容器

首先,我们要通过执行下面的命令来停止容器的运行。

$ docker kill 9a8a8e6a13ae

使用Visual Studio Code进行调试

Visual Studio Code为在Docker容器中运行的Node.js应用程序提供了调试工具。

下面是我们可以使用这些工具的步骤。

第1步:更新Docker文件

编辑Dockerfile ,替换下面这一行。

CMD ["node", "app.js"]

用。

CMD ["npm","run", "start-debug"]

我们更新后的Dockerfile ,现在看起来如下图所示。

FROM node:14
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "run" , "start-debug" ]

第2步:编辑package.json文件

编辑package.json 文件,添加下面这行代码。

"start-debug": "node --inspect=0.0.0.0 app.js"

上面这行代码初始化了Node.js进程以监听运行在9229端口的调试客户端。

更新后的package.json 文件现在看起来会是这样。

{
  "name": "TodoApp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
	"start-debug": "node --inspect=0.0.0.0 app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cookie-session": "^1.4.0",
    "ejs": "^3.1.6",
    "express": "^4.17.1"
  }
}

第3步:构建Docker镜像

注意,每次Dockerfile 被更新,我们都必须再次构建Docker镜像。

$ docker build -t to-do-app .

另外,请注意,Docker现在将运行我们之前更新的npm run start-debug 命令。

第4步:运行Docker容器

为了使用Visual Studio Code进行调试,我们必须转发9229端口。

这可以通过运行下面的命令来完成。

$ docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret22222 -d to-do-app

上述命令将产生以下输出。

$ docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret22222 -d to-do-app

f2484b712a78084b2df7e07a8b72a8d2736fd81ed75e678459a703b4812385bb

第5步:检查Docker日志

我们可以通过运行docker logs 命令并指定容器的id 来检查日志,如下图所示。

$ docker logs f2484b712a78084b2df7e07a8b72a8d2736fd81ed75e678459a703b4812385bb

> TodoApp@1.0.0 start-debug /usr/src/app
> node --inspect=0.0.0.0 app.js

Debugger listening on ws://0.0.0.0:9229/6a1580dd-ef36-4db3-b7b6-40b3f37f41d1
For help, see: https://nodejs.org/en/docs/inspector
MyTodo app is listening on port 3000!

注意,调试器现在正在监听端口9229。接下来,我们将配置Visual Studio Code来调试我们的应用程序。

使用Visual Studio Code调试应用程序时的步骤

启动Visual Studio Code,并打开项目目录,如下图。

project-directory

第1步:配置Visual Studio Code

Visual Studio Code中的调试配置存储在一个名为launch.json 的文件中。要访问它,按Ctrl+Shift+P ,然后搜索Debug: Open launch.json ,并打开它。

编辑launch.json 文件,使其看起来像下面的代码片断。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Docker: Attach to Node",
            "type": "node",
            "request": "attach",
            "port": 9229,
            "address": "localhost",
            "localRoot": "${workspaceFolder}",
            "remoteRoot": "/usr/src/app",
            "protocol": "inspector",
            "skipFiles": [
              "${workspaceFolder}/node_modules/**/*.js",
              "<node_internals>/**/*.js"
            ]
        }
    ]
  }

由于我们不想翻阅node_modules 目录和内置Node.js核心模块中的代码,我们已经包含了skipFiles 属性。

第2步:放置一个断点来调试代码

现在我们已经设置好了一切,我们可以开始调试应用程序。如果你还记得前面的内容,我们在view/todo.ejs 文件中出现了一个错误,其中下面这行代码超过了todolist 数组。

todolist.forEach(function(todo, index)

app.js 文件中,todo.ejs 文件在这一行得到了渲染res.render('todo.ejs', { todolist: req.session.todolist }) 。我们将在该行设置一个断点,并检查todolist 变量的值,如下图所示。

breakpoint

第3步:插入一个表达式来检查变量的值

Shift+Ctrl+D ,切换到调试视图。

然后点击Debug and Run 按钮,如下图所示。

debug-run

为了检查req.session.todolist 变量的值,我们将插入一个表达式来观察。其方法是在窗口的侧边栏上选择watch下的+ 标志,然后输入变量的名称(req.session.todolist),如下图所示。

debug-watch

第4步:在网络浏览器上测试该应用程序

接下来,切换到网络浏览器,刷新页面http://localhost:3001/todo,如下图所示。

waiting for localhost

Waiting for localhost 信息将出现在页面的左下方,这意味着我们创建的断点已经暂停了执行,我们可以检查req.session.todolist 变量的值。

现在移回Visual Studio Code,获得如图所示的细节。

debug-watch-value

所以req.session.todolist 变量是undefined 。是否可以修复这个bug?

第5步:修复该错误

ejb 模板经过todolist 数组,应该被放置在当前会话中。然而,我们没有初始化这个数组,所以它是未定义的。

为了修复这个错误,我们将在.use 函数中加入下面几行代码。

if (typeof (req.session.todolist) == 'undefined') {
  req.session.todolist = []
}

这段代码应该添加在next 函数的正上方。

我们的.use 函数将看起来如下。

app.use(session({ secret: process.env.SECRET }))
  .use(function (req, res, next) {
    if (typeof (req.session.todolist) == 'undefined') {
      req.session.todolist = []
  }
    next()
  })

第6步:停止Docker容器

接下来,我们将通过执行下面的docker ps 命令来检索我们正在运行的容器的id

$ docker ps

CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                                                                  NAMES
f2484b712a78   to-do-app   "docker-entrypoint.s…"   5 minutes ago   Up 5 minutes   0.0.0.0:9229->9229/tcp, :::9229->9229/tcp, 0.0.0.0:3001->3000/tcp, :::3001->3000/tcp   jovial_swartz

通过运行docker kill ,然后再运行其id ,停止容器,如下图。

$ docker kill f2484b712a78

f2484b712a78

第7步:重新构建Docker镜像

为了应用我们之前所做的改变,我们必须重新运行docker build ,如下图所示。

$ docker build -t to-do-app .

第8步:重新运行Docker容器

接下来,使用下面的命令运行该容器。

$ docker run -p 3001:3000 -p 9229:9229 -e SECRET=bestkeptsecret22222 -d to-do-app

第9步:重新加载网络浏览器以确认是否修复了一个错误

在浏览器上重新加载https://localhost:3001/todo页面,并检查结果,如下图所示。

todo-app-success

总结

我们已经成功地编写了一个待办事项列表Node.js应用程序,在Docker容器中执行,并利用我们的Visual Studio Code调试工具来识别和修复这个问题。

本教程对那些想在Docker容器中构建Node.js应用程序并利用Visual Studio代码调试工具来调试代码的开发者有帮助。