如何使用 Docker 镜像 一个 Node.js Web 应用程序

Docker 是一个容器化平台,可简化应用程序的打包和执行。容器作为具有自己文件系统的独立进程运行,但共享其主机的内核。 Docker 作为一种实现可重现的开发环境和分布式部署架构的方式而备受瞩目。

Node.js 是用于后端开发的领先的 JavaScript 运行语言。成功启动 Node.js Web 服务需要我们有一个安装了 Node 的环境、可用的应用程序代码以及在发生崩溃时处理自动重启的机制。

在本篇文章中,我们将使用 Docker 将使用流行的 Express Web 框架创建的简单 Node.js 应用程序容器化。 Docker 是部署基于节点的系统的好方法,因为它创造了一个一致的环境,其中包括运行服务所需的一切。 Docker 守护程序集成了对在其前台进程崩溃时重新启动失败容器的支持,从而解决了 Node.js 部署的问题。

创建 Node 项目

我们将跳过实现应用程序的细节。 为项目创建一个目录并在其中添加一些服务器代码。 这是一个基本的 app.js,它监听端口 8080 并使用硬编码响应来响应每个请求:

const express = require("express");
 
const app = express();
app.get("*", (req, res) => res.send("<p>Hello 迹忆客!</p>"));
app.listen(8080, () => console.log("Listening on 8080"));

使用 npm 将 Express 添加到项目中:

$ npm init
$ npm install --save express

启动应用程序来看它是否有效:

$ node app.js

docker 镜像 node

我们应该能够在浏览器中通过访问 localhost:8080 来查看效果。

docker 镜像 node 查看结果

编写 Dockerfile

现在开始镜像打包我们的项目了。 首先,我们需要为应用程序提供镜像。 镜像将我们的代码和依赖项封装为用于启动容器实例的单个包。 Dockerfile 中的指令定义了容器初始文件系统的状态。

下面是适用于示例应用程序的 Dockerfile:

FROM node:16
WORKDIR /app

COPY package.json .
COPY package-lock.json .
RUN npm ci

COPY app.js .
CMD ["app.js"]

此 Dockerfile 通过 FROM 语句选择官方的 Node.js Docker 镜像作为其基础。 镜像继承基础中的所有内容,然后通过以下说明添加其他内容。

WORKDIR 行将工作目录设置为 /app。 COPY 语句会将文件存放到容器镜像内的 /app 目录中。

安装依赖

下一阶段是添加 npm 的 package.json 并运行 npm ci。 这将在容器的文件系统中安装项目的 npm 依赖项(在本例中为 Express)。

不要使用 COPY node_modules/ . 复制项目目录中现有的 node_modules 文件夹——这将阻止我们在其他构建环境中重用 Dockerfile。 Dockerfiles 应该允许我们仅使用源代码控制存储库的内容创建一致的构建。 如果我们的 .gitignore 中有文件或文件夹,则不应在 Dockerfile COPY 指令中引用它。

复制应用程序代码

npm ci 运行后,我们的应用程序代码将复制到镜像中。 将此 COPY 指令放置在 RUN 之后,将其与先前的副本分开,是经过深思熟虑的。 每条指令都会在镜像中创建一个新镜像; Docker 的构建过程缓存每一层来加速后续构建。 一旦某一层的内容发生变化,后续所有层的缓存都会失效。

这就是为什么在执行 npm ci 之后应该复制应用程序代码的原因。 代码通常会比 npm 锁定文件的内容更频繁地更改。 仅涉及代码更改的映像重建将有效地跳过 RUN npm ci 阶段(以及所有早期阶段),当我们有很多依赖项时,会大大加快该过程。

设置镜像的命令

最后的 Dockerfile 阶段使用 CMD 指令在容器启动时自动运行我们的应用程序。 这是因为 Node.js 基础镜像被配置为使用 node 进程作为其入口点。 CMD 被附加到继承的入口点,导致 node app.js 作为新镜像的前台进程运行。


创建镜像

接下来,我们需要构建镜像:

$ docker build -t node-app:latest .

Docker 将获取工作目录中的 Dockerfile,运行其中的指令,并将生成的镜像标记为 node-app:latest。 最后将我们的工作目录指定为映像构建上下文。 这决定了 Dockerfile 中的 COPY 指令可以引用的路径。

构建优化

提高构建性能的一种方法是将 .dockerignore 文件添加到项目的根目录。 将下面内容添加到文件中:

node_modules/

该文件定义了工作目录中不会包含在构建上下文中的路径。 因此我们将无法在 Dockerfile 中引用它们。 对于 node_modules,该目录的内容与构建无关,因为我们正在通过 RUN npm ci 指令重新安装依赖项。 特别排除工作目录中已经存在的 node_modules 可以节省将所有这些文件复制到 Docker 的临时构建上下文位置的操作。 这提高了效率并减少了准备构建所花费的时间。


启动容器

此时我们已准备好使用 Docker 运行自己的应用程序:

$ docker run -d \
    -p 8080:8080 \
    --name my-app \
    --restart on-failure \
    node-app:latest

docker 镜像 node 容器

docker run 命令用于从指定的镜像启动一个新的容器实例。添加了一些额外的标志来为预期的用例正确配置容器:

  • -d – 将 shell 从容器的前台进程中分离出来,有效地将其作为后台服务器运行。
  • -p – 将主机上的端口 8080 绑定到容器内的端口 8080(我们的 Express 示例应用程序被配置为监听8080)。这意味着到 localhost:8080 的流量将被传递到相应的容器端口。可以通过修改绑定定义的第一部分来将主机更改为不同的值,例如 8100:8080 来访问 localhost:8100 上的容器。
  • –name – 为容器分配一个名称,可以使用该名称在其他 Docker CLI 命令中引用它。
  • –restart – 选择要应用于容器的重启策略。 on-failure 设置意味着如果容器因应用程序崩溃而退出并显示失败代码,Docker 将自动重启容器。

上一步中构建的镜像被用作 docker run 命令的最后一个参数。 容器 ID 将发送到终端窗口; 我们应该能够通过再次访问 localhost:8080 来访问 Node.js 应用程序。 这次服务器在 Docker 容器内运行,而不是使用安装在主机上的节点进程。

docker 镜像 node 查看结果


总结

Docker 通过容器化整个应用程序环境来帮助我们部署 Node.js Web 服务。我们可以在安装了 Docker 的任何主机上使用单个 docker run 命令从镜像启动容器。这消除了维护 Node.js 版本、安装 npm 模块和监控应用程序进程需要重新启动的情况的复杂性。

当我们进行代码更改并想要启动更新时,需要重建 Docker 镜像并使用 docker rm <container-name> 删除旧容器。然后,我们可以启动使用修改后的镜像的替换实例。

我们可能希望在生产中使用稍微不同的例程。尽管可以使用带有 docker run 的常规 Docker 安装,但这对于除了最简单的应用程序之外的所有应用程序来说往往是笨拙的。更常见的是使用 Docker Compose 或 Kubernetes 之类的工具在可以在存储库中进行版本控制的文件中定义容器配置。

这些机制消除了每次启动新容器时重复 docker run 命令的需要。它们还有助于容器复制以扩展服务并提供冗余。如果要部署到远程主机,则还需要将镜像推送到 Docker 注册表,以便可以从生产机器中“拉取”它。

另一个特定于生产的考虑因素是如何将流量路由到容器。 端口绑定一开始就足够了,但最终你会遇到这样一种情况,即你希望在一台主机上有多个容器,每个容器都在同一个端口上侦听。 在这种情况下,我们可以部署反向代理以根据请求特征(例如域名和标头)将流量路由到各个容器端口。

Docker 是一个容器化平台,可简化应用程序的打包和执行。容器作为具有自己文件系统的独立进程运行,但共享其主机的内核。 Docker 作为一种实现可重现的开发环境和分布式部署架构的方式而备受瞩目。

Node.js 是用于后端开发的领先的 JavaScript 运行语言。成功启动 Node.js Web 服务需要我们有一个安装了 Node 的环境、可用的应用程序代码以及在发生崩溃时处理自动重启的机制。

在本篇文章中,我们将使用 Docker 将使用流行的 Express Web 框架创建的简单 Node.js 应用程序容器化。 Docker 是部署基于节点的系统的好方法,因为它创造了一个一致的环境,其中包括运行服务所需的一切。 Docker 守护程序集成了对在其前台进程崩溃时重新启动失败容器的支持,从而解决了 Node.js 部署的问题。

创建 Node 项目

我们将跳过实现应用程序的细节。 为项目创建一个目录并在其中添加一些服务器代码。 这是一个基本的 app.js,它监听端口 8080 并使用硬编码响应来响应每个请求:

const express = require("express");
 
const app = express();
app.get("*", (req, res) => res.send("<p>Hello 迹忆客!</p>"));
app.listen(8080, () => console.log("Listening on 8080"));

使用 npm 将 Express 添加到项目中:

$ npm init
$ npm install --save express

启动应用程序来看它是否有效:

$ node app.js

docker 镜像 node

我们应该能够在浏览器中通过访问 localhost:8080 来查看效果。

docker 镜像 node 查看结果

编写 Dockerfile

现在开始镜像打包我们的项目了。 首先,我们需要为应用程序提供镜像。 镜像将我们的代码和依赖项封装为用于启动容器实例的单个包。 Dockerfile 中的指令定义了容器初始文件系统的状态。

下面是适用于示例应用程序的 Dockerfile:

FROM node:16
WORKDIR /app

COPY package.json .
COPY package-lock.json .
RUN npm ci

COPY app.js .
CMD ["app.js"]

此 Dockerfile 通过 FROM 语句选择官方的 Node.js Docker 镜像作为其基础。 镜像继承基础中的所有内容,然后通过以下说明添加其他内容。

WORKDIR 行将工作目录设置为 /app。 COPY 语句会将文件存放到容器镜像内的 /app 目录中。

安装依赖

下一阶段是添加 npm 的 package.json 并运行 npm ci。 这将在容器的文件系统中安装项目的 npm 依赖项(在本例中为 Express)。

不要使用 COPY node_modules/ . 复制项目目录中现有的 node_modules 文件夹——这将阻止我们在其他构建环境中重用 Dockerfile。 Dockerfiles 应该允许我们仅使用源代码控制存储库的内容创建一致的构建。 如果我们的 .gitignore 中有文件或文件夹,则不应在 Dockerfile COPY 指令中引用它。

复制应用程序代码

npm ci 运行后,我们的应用程序代码将复制到镜像中。 将此 COPY 指令放置在 RUN 之后,将其与先前的副本分开,是经过深思熟虑的。 每条指令都会在镜像中创建一个新镜像; Docker 的构建过程缓存每一层来加速后续构建。 一旦某一层的内容发生变化,后续所有层的缓存都会失效。

这就是为什么在执行 npm ci 之后应该复制应用程序代码的原因。 代码通常会比 npm 锁定文件的内容更频繁地更改。 仅涉及代码更改的映像重建将有效地跳过 RUN npm ci 阶段(以及所有早期阶段),当我们有很多依赖项时,会大大加快该过程。

设置镜像的命令

最后的 Dockerfile 阶段使用 CMD 指令在容器启动时自动运行我们的应用程序。 这是因为 Node.js 基础镜像被配置为使用 node 进程作为其入口点。 CMD 被附加到继承的入口点,导致 node app.js 作为新镜像的前台进程运行。


创建镜像

接下来,我们需要构建镜像:

$ docker build -t node-app:latest .

Docker 将获取工作目录中的 Dockerfile,运行其中的指令,并将生成的镜像标记为 node-app:latest。 最后将我们的工作目录指定为映像构建上下文。 这决定了 Dockerfile 中的 COPY 指令可以引用的路径。

构建优化

提高构建性能的一种方法是将 .dockerignore 文件添加到项目的根目录。 将下面内容添加到文件中:

node_modules/

该文件定义了工作目录中不会包含在构建上下文中的路径。 因此我们将无法在 Dockerfile 中引用它们。 对于 node_modules,该目录的内容与构建无关,因为我们正在通过 RUN npm ci 指令重新安装依赖项。 特别排除工作目录中已经存在的 node_modules 可以节省将所有这些文件复制到 Docker 的临时构建上下文位置的操作。 这提高了效率并减少了准备构建所花费的时间。


启动容器

此时我们已准备好使用 Docker 运行自己的应用程序:

$ docker run -d \
    -p 8080:8080 \
    --name my-app \
    --restart on-failure \
    node-app:latest

docker 镜像 node 容器

docker run 命令用于从指定的镜像启动一个新的容器实例。添加了一些额外的标志来为预期的用例正确配置容器:

  • -d – 将 shell 从容器的前台进程中分离出来,有效地将其作为后台服务器运行。
  • -p – 将主机上的端口 8080 绑定到容器内的端口 8080(我们的 Express 示例应用程序被配置为监听8080)。这意味着到 localhost:8080 的流量将被传递到相应的容器端口。可以通过修改绑定定义的第一部分来将主机更改为不同的值,例如 8100:8080 来访问 localhost:8100 上的容器。
  • –name – 为容器分配一个名称,可以使用该名称在其他 Docker CLI 命令中引用它。
  • –restart – 选择要应用于容器的重启策略。 on-failure 设置意味着如果容器因应用程序崩溃而退出并显示失败代码,Docker 将自动重启容器。

上一步中构建的镜像被用作 docker run 命令的最后一个参数。 容器 ID 将发送到终端窗口; 我们应该能够通过再次访问 localhost:8080 来访问 Node.js 应用程序。 这次服务器在 Docker 容器内运行,而不是使用安装在主机上的节点进程。

docker 镜像 node 查看结果


总结

Docker 通过容器化整个应用程序环境来帮助我们部署 Node.js Web 服务。我们可以在安装了 Docker 的任何主机上使用单个 docker run 命令从镜像启动容器。这消除了维护 Node.js 版本、安装 npm 模块和监控应用程序进程需要重新启动的情况的复杂性。

当我们进行代码更改并想要启动更新时,需要重建 Docker 镜像并使用 docker rm <container-name> 删除旧容器。然后,我们可以启动使用修改后的镜像的替换实例。

我们可能希望在生产中使用稍微不同的例程。尽管可以使用带有 docker run 的常规 Docker 安装,但这对于除了最简单的应用程序之外的所有应用程序来说往往是笨拙的。更常见的是使用 Docker Compose 或 Kubernetes 之类的工具在可以在存储库中进行版本控制的文件中定义容器配置。

这些机制消除了每次启动新容器时重复 docker run 命令的需要。它们还有助于容器复制以扩展服务并提供冗余。如果要部署到远程主机,则还需要将镜像推送到 Docker 注册表,以便可以从生产机器中“拉取”它。

另一个特定于生产的考虑因素是如何将流量路由到容器。 端口绑定一开始就足够了,但最终你会遇到这样一种情况,即你希望在一台主机上有多个容器,每个容器都在同一个端口上侦听。 在这种情况下,我们可以部署反向代理以根据请求特征(例如域名和标头)将流量路由到各个容器端口。

Docker 是一个容器化平台,可简化应用程序的打包和执行。容器作为具有自己文件系统的独立进程运行,但共享其主机的内核。 Docker 作为一种实现可重现的开发环境和分布式部署架构的方式而备受瞩目。

Node.js 是用于后端开发的领先的 JavaScript 运行语言。成功启动 Node.js Web 服务需要我们有一个安装了 Node 的环境、可用的应用程序代码以及在发生崩溃时处理自动重启的机制。

在本篇文章中,我们将使用 Docker 将使用流行的 Express Web 框架创建的简单 Node.js 应用程序容器化。 Docker 是部署基于节点的系统的好方法,因为它创造了一个一致的环境,其中包括运行服务所需的一切。 Docker 守护程序集成了对在其前台进程崩溃时重新启动失败容器的支持,从而解决了 Node.js 部署的问题。

创建 Node 项目

我们将跳过实现应用程序的细节。 为项目创建一个目录并在其中添加一些服务器代码。 这是一个基本的 app.js,它监听端口 8080 并使用硬编码响应来响应每个请求:

const express = require("express");
 
const app = express();
app.get("*", (req, res) => res.send("<p>Hello 迹忆客!</p>"));
app.listen(8080, () => console.log("Listening on 8080"));

使用 npm 将 Express 添加到项目中:

$ npm init
$ npm install --save express

启动应用程序来看它是否有效:

$ node app.js

docker 镜像 node

我们应该能够在浏览器中通过访问 localhost:8080 来查看效果。

docker 镜像 node 查看结果

编写 Dockerfile

现在开始镜像打包我们的项目了。 首先,我们需要为应用程序提供镜像。 镜像将我们的代码和依赖项封装为用于启动容器实例的单个包。 Dockerfile 中的指令定义了容器初始文件系统的状态。

下面是适用于示例应用程序的 Dockerfile:

FROM node:16
WORKDIR /app

COPY package.json .
COPY package-lock.json .
RUN npm ci

COPY app.js .
CMD ["app.js"]

此 Dockerfile 通过 FROM 语句选择官方的 Node.js Docker 镜像作为其基础。 镜像继承基础中的所有内容,然后通过以下说明添加其他内容。

WORKDIR 行将工作目录设置为 /app。 COPY 语句会将文件存放到容器镜像内的 /app 目录中。

安装依赖

下一阶段是添加 npm 的 package.json 并运行 npm ci。 这将在容器的文件系统中安装项目的 npm 依赖项(在本例中为 Express)。

不要使用 COPY node_modules/ . 复制项目目录中现有的 node_modules 文件夹——这将阻止我们在其他构建环境中重用 Dockerfile。 Dockerfiles 应该允许我们仅使用源代码控制存储库的内容创建一致的构建。 如果我们的 .gitignore 中有文件或文件夹,则不应在 Dockerfile COPY 指令中引用它。

复制应用程序代码

npm ci 运行后,我们的应用程序代码将复制到镜像中。 将此 COPY 指令放置在 RUN 之后,将其与先前的副本分开,是经过深思熟虑的。 每条指令都会在镜像中创建一个新镜像; Docker 的构建过程缓存每一层来加速后续构建。 一旦某一层的内容发生变化,后续所有层的缓存都会失效。

这就是为什么在执行 npm ci 之后应该复制应用程序代码的原因。 代码通常会比 npm 锁定文件的内容更频繁地更改。 仅涉及代码更改的映像重建将有效地跳过 RUN npm ci 阶段(以及所有早期阶段),当我们有很多依赖项时,会大大加快该过程。

设置镜像的命令

最后的 Dockerfile 阶段使用 CMD 指令在容器启动时自动运行我们的应用程序。 这是因为 Node.js 基础镜像被配置为使用 node 进程作为其入口点。 CMD 被附加到继承的入口点,导致 node app.js 作为新镜像的前台进程运行。


创建镜像

接下来,我们需要构建镜像:

$ docker build -t node-app:latest .

Docker 将获取工作目录中的 Dockerfile,运行其中的指令,并将生成的镜像标记为 node-app:latest。 最后将我们的工作目录指定为映像构建上下文。 这决定了 Dockerfile 中的 COPY 指令可以引用的路径。

构建优化

提高构建性能的一种方法是将 .dockerignore 文件添加到项目的根目录。 将下面内容添加到文件中:

node_modules/

该文件定义了工作目录中不会包含在构建上下文中的路径。 因此我们将无法在 Dockerfile 中引用它们。 对于 node_modules,该目录的内容与构建无关,因为我们正在通过 RUN npm ci 指令重新安装依赖项。 特别排除工作目录中已经存在的 node_modules 可以节省将所有这些文件复制到 Docker 的临时构建上下文位置的操作。 这提高了效率并减少了准备构建所花费的时间。


启动容器

此时我们已准备好使用 Docker 运行自己的应用程序:

$ docker run -d \
    -p 8080:8080 \
    --name my-app \
    --restart on-failure \
    node-app:latest

docker 镜像 node 容器

docker run 命令用于从指定的镜像启动一个新的容器实例。添加了一些额外的标志来为预期的用例正确配置容器:

  • -d – 将 shell 从容器的前台进程中分离出来,有效地将其作为后台服务器运行。
  • -p – 将主机上的端口 8080 绑定到容器内的端口 8080(我们的 Express 示例应用程序被配置为监听8080)。这意味着到 localhost:8080 的流量将被传递到相应的容器端口。可以通过修改绑定定义的第一部分来将主机更改为不同的值,例如 8100:8080 来访问 localhost:8100 上的容器。
  • –name – 为容器分配一个名称,可以使用该名称在其他 Docker CLI 命令中引用它。
  • –restart – 选择要应用于容器的重启策略。 on-failure 设置意味着如果容器因应用程序崩溃而退出并显示失败代码,Docker 将自动重启容器。

上一步中构建的镜像被用作 docker run 命令的最后一个参数。 容器 ID 将发送到终端窗口; 我们应该能够通过再次访问 localhost:8080 来访问 Node.js 应用程序。 这次服务器在 Docker 容器内运行,而不是使用安装在主机上的节点进程。

docker 镜像 node 查看结果


总结

Docker 通过容器化整个应用程序环境来帮助我们部署 Node.js Web 服务。我们可以在安装了 Docker 的任何主机上使用单个 docker run 命令从镜像启动容器。这消除了维护 Node.js 版本、安装 npm 模块和监控应用程序进程需要重新启动的情况的复杂性。

当我们进行代码更改并想要启动更新时,需要重建 Docker 镜像并使用 docker rm <container-name> 删除旧容器。然后,我们可以启动使用修改后的镜像的替换实例。

我们可能希望在生产中使用稍微不同的例程。尽管可以使用带有 docker run 的常规 Docker 安装,但这对于除了最简单的应用程序之外的所有应用程序来说往往是笨拙的。更常见的是使用 Docker Compose 或 Kubernetes 之类的工具在可以在存储库中进行版本控制的文件中定义容器配置。

这些机制消除了每次启动新容器时重复 docker run 命令的需要。它们还有助于容器复制以扩展服务并提供冗余。如果要部署到远程主机,则还需要将镜像推送到 Docker 注册表,以便可以从生产机器中“拉取”它。

另一个特定于生产的考虑因素是如何将流量路由到容器。 端口绑定一开始就足够了,但最终你会遇到这样一种情况,即你希望在一台主机上有多个容器,每个容器都在同一个端口上侦听。 在这种情况下,我们可以部署反向代理以根据请求特征(例如域名和标头)将流量路由到各个容器端口。