我读了一些 Docker 和 Node.js 最佳实践文章,例如https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md 或 使用 Docker 容器化 Node.js Web 应用程序的 10 个最佳实践 或 适用于 Node 和 NPM 的 Dockerfile 良好实践 。所有这些文章至少是在 2021 年写或更新的,我不列出 2021 年之前写的文章,但也有不少。
他们都反对
CMD ["npm", "run", "start"]
。主要原因是 npm 会“吞掉”退出信号,例如 SIGTERM 和 SIGINT,因此我的节点应用程序中的正常关闭代码将无法运行。
我猜旧的 npm 就是这种情况(虽然我没有测试它),但我已经测试了 node14+npm6 和 node16+npm8,我可以验证 npm6/8 确实 NOT吞下这些事件和我的优雅运行关闭代码。不确定这是否是因为 npm 修复了它。 所以唯一的问题仍然是还有 1 个进程 npm 要运行,即 NPM 以
PID 1运行。有些文章说问题是“PID 1 不会响应 SIGINT”,但我已经证实事实并非如此。 许多文章(例如这个
nodejs doc)仅建议CMD [ "node", "server.js" ]
,但也在
https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals说“以 PID 1 运行的 Node.js 进程不会响应 SIGINT (CTRL-C) 和类似信号。”,即 Nodejs 自己的文档本身矛盾(但我确实看到 NodeJS 作为 PID 1 响应 SIGINT) 所以我对
CMD ["npm", "run", "start"]
或
CMD [ "node", "server.js" ]
的问题感到困惑对于我的应用程序,还有 1 个考虑因素,我的 npm 脚本具有预挂钩以使应用程序正确运行,我有
prestart
npm 脚本使
npm start
工作。所以目前我只使用 CMD ["npm", "run", "start"]
但我对如何在 docker 中启动我的节点应用程序的“最佳实践”感到困惑。---更新---
我发现了 npm
lifecycle 的已解决问题:将 SIGTERM 传播给子进程确实修复了它,但该问题的最新评论是在2017,其中说“是的,这不起作用,至少对于bash;npm在shell中运行其生命周期过程,而bash不会将 SIGTERM 转发给其子级。” 我意识到我只在我的 mac 和 CentOS 服务器以及基于 alpine 的 docker 上进行了测试。也可能是因为我在CMD中使用exec形式,而不是shell形式,所以我得到了退出信号。
Node.js 和 Kubernetes 的正常关闭说他们的 alpine 镜像没有使用 npm start
获得 SIGTERM,而我在 alpine3.15 上测试,我可以得到。
CMD [ "bash", "-c", "npm run db:migrate && node ./dist/server/index.js" ]
其中
npm run db:migrate && node ./dist/server/index.js
是我的
npm start
的内容
CMD [ "npm", "run", "start" ]
查看docker容器上的进程图
$ ps ajxf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 pts/0 1 Ssl+ 0 0:01 npm run start
1 19 1 1 pts/0 1 S+ 0 0:00 sh -c -- node server.js
19 20 1 1 pts/0 1 Sl+ 0 0:00 \_ node server.js
npm
进程会产生一个
shell
进程,然后再生成 node
进程。这意味着 npm
不会将 node
进程作为直接子进程生成。导致npm
进程无法将信号传递给
node
进程。这与 npm
在本地的行为不同,它直接生成
node
进程。
带节点
CMD [ "node", "server.js" ]
流程图
$ ps ajxf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 pts/0 1 Ssl+ 0 0:00 node server.js
解决方案:
CMD [ "bash", "-c", "node server.js" ]
或者使用
tini
/
s6
初始化系统的节点 Docker,例如18(或 18-bookworm)、18-slim(或 18-bookworm-slim)、18-bullseye、18-bullseye-slim、18-buster、18-buster-slim 不响应 SIGINT 信号。 我已经测试了来自节点 14+ 的所有基于 alpine 的 docker,它们都响应 SIGINT 信号。所以对我来说,这是选择基于 alpine 的镜像而不是基于 Debian 的 slim 镜像的另一个原因。