面向WEB开发人员的Docker(七):使用 Docker 开发Node应用程序

1,825 阅读8分钟

【这是我参与更文挑战的第3天,活动详情查看: 更文挑战”】

到目前为止,已经使用了预构建的 Docker 镜像,例如MySQLVueNginxWordPress。都比较实用,本节来介绍在容器中开发NodeJs应用程序。

在本节中,将创建一个Node.js的“Hello”应用程序,并将该应用构建成Docker镜像,并从容器启动。正常情况下,该镜像可以部署到生产服务器上,Docker Compose 将用于覆盖一些设置以创建开发和调试环境。这样可以在主机PC上进行编码开发,这样文件将在一个持续运行的容器中执行。这有几个好处:

  • Docker 将管理所有依赖项—— 不需要安装和维护runtimes
  • 这个过程与本地开发没什么不同——可以使用任何喜欢的编辑器和工具
  • 容器是隔离的——应用程序影响到主机PC,如使删除文件
  • 任何时候都可以将应用程序分发给其他开发人员或测试人员——应用程序可以在任何其他设备上以零配置相同的方式运行。

本节创建的代码文件在项目 https://github.com/QuintionTang/docker-nodejs

基于容器的应用开发

Docker 简化了 Web 开发:任何的 Web 应用程序都可以在单个容器中运行。

但是……如果想将类似的容器部署到实时生产服务器,应用程序通常是无状态的。这样可以启动任意数量的实例,任何实例都可以对请求做出响应。实际上,应用程序不应该将基本状态数据存储在本地文件或内存中。

例如:当用户登录时,应用程序将登录凭据存储在内存中。在开发过程中使用单个容器,都可以按预期运行没有问题。

如果将应用程序部署到生产服务器并在两个以上容器中运行,这些容器通过负载均衡接收请求。用户访问系统由 container1 处理其登录。那么下一个请求可能就由 container2 提供服务,容器之间并没有共享登录状态,这个时候就会出现未登录的情况。

当然上面的问题是可以通过解决的,为隔离的容器提供一个中心存储服务,维护应用的持久化存储数据,例如数据库。

无状态 Web 应用程序是一个不错的方式。这样在生产环境中随着用户情况的增加可以快速进行扩缩容,自动添加更多的机器/容器。在解决实际需求的时候就需要考虑是否适合无状态,如果对有状态的应用程序进行转换可能是不可行的。

这些在开发过程中都无关紧要,因为通常只会在单个容器中运行应用程序。如果不实用,就不必在生产中使用容器。

什么是 Node.js

这个想必大部份掘金的小伙伴都知道,这里不展开介绍,引用一段简单的说明。

Node.js 是一种流行的、高性能 JavaScript 运行时,使用 Chrome 浏览器的 V8 JavaScript 引擎构建。它通常用于服务器端 Web 开发,但也已被前端或客户端用来构建工具、桌面应用程序、嵌入式系统等所采用。

安装 Node.js 后,可以使用以下命令执行 JavaScript 文件:

node index.js

单入口脚本文件是什么?理论上它可以命名为任何名称,通常项目都使用index.js 作为入口。

前面的内容一直在使用 Docker Hub 提供的 Docker 镜像。本节将介绍如何构建自己的 Docker 镜像,该镜像可以在开发和生产环境中安装和执行应用程序。

可能你对 Node.js 不感兴趣,但是不管使用何种语言(PHP、Python、Ruby、Go、Rust等)都适合使用 Docker 。

Hello应用概述

该项目将使用Node.js的Express.js框架创建了一个“Hello”应用程序。

应用运行地址为:http://localhost:3005/,返回纯文本格式:Hello Devpoint!

从客户端 Ajax 请求调用相同的 URL 会返回 JSON 编码的对象:

{ "message": "Hello Devpoint!" }

当传入请求的HTTP 标头设置为时,可以识别 Ajax 调用。这是由大多数 Ajax 库添加了: X-Requested-WithXMLHttpRequest

可以向 URL 路径添加字符串,例如http://localhost:3005/devpoint 将返回 Hello Devpint!,响应内容为:

{ "message": "Hello Devpoint!" }

项目初始化

在项目目录中执行以下代码初始化项目:

npm init

image.png

输入基本的信息后,会在项目根目录下生成 package.json

接下来安装 express ,执行一下命令:

npm install express --save

为了开发过程中能够响应代码的变更,接下来安装 Nodemon,执行以下命令:

npm install nodemon --save-dev

nodemon 用来监听 node.js 项目中文件的更改并自动重启服务的工具,接下来为项目增加监听规则,如需要忽略的目录:

{
    "script": "./index.js",
    "ext": "js json",
    "ignore": [
        "node_modules/"
    ],
    "legacyWatch": true,
    "delay": 200,
    "verbose": true
}

修改项目 package.json ,在scripts属性下添加启动命令:

"start": "node ./index.js",
"debug": "nodemon --trace-warnings --inspect=0.0.0.0:9229 ./index.js",

这样在终端可以执行一下的命令:

  • npm start : 一般用于生产环境
  • npm run debug :用于开发调试

应用脚本 index.js

脚本在根路由下定义简单的响应请求

"use strict";
const port = process.env.NODE_PORT || 3005, // 定义HTTP默认端口或者从NODE_PORT环境变量获取
    express = require("express"),
    app = express();
// 根路由
app.get("/:title?", (req, res) => {
    const message = `Hello ${req.params.title || "Devpoint"}!`;
    if (req.xhr) {
        res.set("Access-Control-Allow-Origin", "*").json({ message });
    } else {
        res.send(message);
    }
});
// 启动HTTP服务
app.listen(port, () => console.log(`server running on port ${port}`));

接下来开始执行脚本:

npm run debug

打开浏览器输入http://localhost:3005/,可以看到响应的响应,如下

image.png

现在可以尝试去修改脚本 index.js 的内容,当有更新的时候,终端会重启服务,刷新浏览器即可看到更新。

image.png

到目前为止,一个简单的NodeJS应用程序已经完成。接下来将介绍如何在Docker环境里面运行调试。

Docker 配置

前面章节有介绍如何在Docker里面运行应用程序,主要介绍 docker compose,镜像文件一般都是现成的,可以查看《面向WEB开发人员的Docker(六):使用nginx部署静态网站》,或者专栏《面向WEB开发人员的Docker》。

本文将介绍另一种方式,自己制定Docker镜像,一般建议为项目制定两个镜像,一个用于开发调试,一个用于生产环境。

Dockerfiles

Dockerfile 定义了安装和执行应用程序所需的构建环境,一个可以随时方便运行的镜像。

通常从 Docker Hub 基础镜像开始,本文的应用程序需要的 Nodejs 镜像:

image.png

每个标签引用一个单独的镜像(用自己的Dockerfile创建)如Node.js

  • 很多镜像都很大,一般100MB以上,因为它们包含完整的Linux OS操作系统。
  • slim 镜像是一般是精简版Linux OS ,包含运行Node.js所需的最小软件包集。如果希望将Node.js容器部署到有限的空间环境中,这些就会很有用。
  • alpine 镜像基于 Alpine Linux ,通常是5MB左右。如果需要尽可能小的镜像,并且对操作系统库的依赖有限,这个版本就非常有用。

lts-alpine 对于本文的应用程序已经足够了:它提供了一个带有Node.js最新版本的小镜像。

在应用程序的根目录中创建一个Dockerfile,代码如下:

# 基于 Node.js 的 lts镜像
FROM node:lts-alpine

# 定义环境变量
ENV WORKDIR=/data/node/app
ENV NODE_ENV=production
ENV NODE_PORT=3005

# 创建应用程序文件夹并分配权限给 node 用户
RUN mkdir -p $WORKDIR && chown -R node:node $WORKDIR

# 设置工作目录
WORKDIR $WORKDIR

# 设置活动用户
USER node

# 复制 package.json 到工作目录
COPY --chown=node:node package.json $WORKDIR/

# 安装依赖
RUN npm install && npm cache clean --force

# 复制其他文件
COPY --chown=node:node . .

# 暴露主机端口
EXPOSE $NODE_PORT

# 应用程序启动命令
CMD [ "node", "./index.js" ]

基础镜像一般都是从 FROM node:lts-alpine 开始,每一行定义了一个步骤,用于安装和运行 Node.js 应用程序。关于 Dockerfile 的语法可以参阅 Dockerfile 指南。下面列举了一些常见的命令:

命令描述
#注释的开始
FROM指定开始的基础镜像,一般是从 Docker Hub 开始构建镜像
ARG构建参数,与 ENV 作用一至。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量
ENV定义环境变量,在Dockerfile文件后续代码中使用,在容器运行时也可以使用
WORKDIR设置工作目录,即在容器内部的路径
USER用于指定镜像中执行后续命令的用户和用户组
VOLUME挂载数据卷
EXPOSE声明端口
COPY添加文件(夹)到容器
RUN在创建镜像是执行,即使用docker build命令时执行
CMD在运行容器时执行,即使用docker run命令时执行

用户安全

创建镜像时,Dockerfile 命令以 root(超级)用户身份运行。这个操作一般是安全的,因为当发生严重异常事件的时候,可以自动重新启动容器。

当然以更受限制的用户身份运行应用程序更安全,本文示例创建用户 node 来启动应用程序。这样即便应用程序被不法分子恶意控制,它也没有权限操作应用程序所在文件夹以外的文件,将安全风险限制在应用程序所在的文件夹内。

启动命令

最佳的方式就是通过直接调用其可执行文件来启动应用程序:

CMD [ "node", "./index.js" ]

这样可以保证将 STDERR 等系统消息返回给 Docker,以便可以做出相应的响应,如当应用程序崩溃时重新启动容器。

.dockerignore

COPY命令将所有应用程序文件从主机目录复制到Docker镜像,通常情况下不需要复制所有文件,这个时候可以通过 .dockerignore 来定义不需要复制的文件或者文件夹,本实例定义的规则如下:

Dockerfile

.git
.gitignore
.config

.npm
.vscode
node_modules
package-lock.json
README.md

构建镜像

下面就从 Dockerfile 构建铭文 nodehello 的镜像,在根目录下执行一下命令:

docker image build -t nodehello .

命令末尾的 . 点是必不可少的,它代表着应用程序路径。

image.png

确定是否构建成功,可以执行命令 docker image ls nodehello,将看到:

image.png

从镜像启动容器

现在就可以使用一下命令启动 nodehello 镜像

docker run -it --rm --name nodehello -p 3005:3005 nodehello

在浏览器中打开 http://localhost:3005/ 可以看到 Hello Devpoint! 内容。

打开新的终端,启动开发环境,同样在项目根目录下,执行以下命令:

docker run -it --rm  --name nodehello  -p 3005:3005  -p 9229:9229  -e NODE_ENV=development  -v $PWD:/data/node/app --entrypoint '/bin/sh'  nodehello  -c 'npm install && npm run debug'

上面的命令以开发模式启动容器并将主机上的项目目录挂载到容器中的路径 /data/node/app

现在来修改 index.js 文件,将 Devpoint 改为 Juejin,回到浏览器刷新可以看到内容变更为:Hello Juejin!

使用 Chrome 调试 Node.js

谷歌Chrome和基于Chrome的浏览器,如Edge、Opera,都有一个内置的Node.js调试器,确保应用程序运行在一个开发模式容器中,然后启动浏览器输入:chrome://inspect

image.png

如果没有看到Remote Target,请确保选中Discover network targets,单击Configure... 并为 <_229 class="calibre"> 添加一个连接(如果是在另一台设备上运行容器,则添加一个网络地址)。

点击 Target 下方的链接 inspect 启动 DevTools ,切换到 Console,可以日志 server running on port 3005

image.png

切换到 Source 面板,按照提示按下 Ctrl|Cmd + P , 输入 index.js , 选择第一个路径为:/data/node/app/index.js

image.png

添加两个断点,如图:

image.png

回到打开 http://localhost:3005/ 的页面刷新页面,可以看到程序执行到断点。

image.png

接下来,如何调试,应该都很熟悉了,这里不继续展开了。