如何选择 node.js 的 Docker 镜像

15,814 阅读4分钟

node.js 应用一般用 pm2 或者 docker 来部署,如果你打算用 docker 部署的话,可能也会遇到如何选择 node 镜像的问题,官方提供了如下三类选择:

  1. node:<version>:这是官方默认镜像,基于 debian 构建,可指定的版本有:

    • Debian 10(buster) — 当前的稳定版(stable)
    • Debian 9(stretch) — 旧的稳定版(oldstable)
    • Debian 8(jessie) — 更旧的稳定版(oldoldstable)
    • Debian 7(wheezy) — 被淘汰的稳定版

    这些镜像是基于 buildpack-deps 进行构建的,这里可以查看 Dockerfile,此类镜像的优点是安装的依赖很全,例如 curlwget ,缺点是体积过大。

  2. node:<version>-slim :这是删除冗余依赖后的精简版本镜像,同样是基于 debian 构建,体积上比默认镜像小很多,删除了很多公共的软件包,只保留了最基本的 node 运行环境。

  3. node:<version>-alpine:这个版本基于 alpine 镜像构建,比 debian 的 slim 版本还要小,可以说是最小的 node 镜像。虽然体积小,但是功能不少,普通的 node.js 应用都能跑起来,但是如果项目中用到了 c++ 扩展的话,就不要用这个了,因为 alpine 使用 musl 代替 glibc,一些 c/c++ 环境的软件可能不兼容。

接下来,基于 node.js 14 版本,分别下载上述三类镜像进行对比:

docker pull node:14-buster
docker pull node:14-buster-slim
docker pull node:14-alpine

镜像体积大小对比

运行 docker images | grep node

node           14-buster           70c62b76e4cc        5 hours ago         912MB
node           14-buster-slim      9917d232c3dd        5 hours ago         181MB
node           14-alpine           9db54a688554        5 hours ago         117MB

可以看到默认镜像 node:14-buster 体积要 912MB ,实在太大了,相较而言 node:14-buster-slim 小很多,而 node:14-alpine 则更为轻巧。

容器内存占用对比

用上面的镜像分别启动容器:

docker run -d --name node-14-buster node:14-buster node -e "require('http').createServer((req, res) => res.end('Hello World')).listen(3030)"

docker run -d --name node-14-buster-slim node:14-buster-slim node -e "require('http').createServer((req, res) => res.end('Hello World')).listen(3030)"

docker run -d --name node-14-alpine node:14-alpine node -e "require('http').createServer((req, res) => res.end('Hello World')).listen(3030)"

运行 docker stats 查看运行时的内存占用

NAME                  CPU %   MEM USAGE/LIMIT     MEM %   NET I/O   BLOCK I/O    PIDS
node-14-alpine        0.00%   4.809MiB/1.796GiB   0.26%   0B/0B     0B/0B        7
node-14-buster-slim   0.00%   4.238MiB/1.796GiB   0.23%   0B/0B     0B/0B        7
node-14-buster        0.00%   4.207MiB/1.796GiB   0.23%   0B/0B     4.88MB/0B    7

差别不大,反而是 alpine 占用内存稍稍高那么一点点,但都在可以接受的范围内。

如何选择?

站在 node.js 应用的角度,应该如何选择镜像呢?alpine 与 buster/buster-slim 最大的差异在于 C++ 插件,例如你的包里面用了 sharp 这个包对图片进行加工处理的话,alpine 镜像就不能用了,因为不兼容。其他情况下,如果你的应用和依赖是纯粹 node.js 编写的,不涉及到 C++ 插件,建议使用 alpine 镜像。

buster/buster-slim 镜像可以运行所有 node.js 项目,包括那些 C++ 依赖,但是有一个坑就是通过 npm start 启动的项目无法监听到 docker 的 SIGTERM 信号,如果进程没有相应 SIGTERM 事件,docker 默认等待 10s,然后就强制杀掉应用了,测试步骤如下:

  1. 首先在根目录创建一个 code 文件夹,写入 index.js

    console.log('pid', process.pid)
    process.on('exit', (code) => {
      console.log('进程 exit 事件的代码: ', code)
    })
    process.on('SIGTERM', (code) => {
      console.log('SIGTERM', code)
      process.exit(0)
    })
    require('http').createServer((req, res) => res.end('Hello World')).listen(3030)
    
  2. 然后创建 package.json :

    {
      "scripts": {
        "start": "node index.js"
      }
    }
    
  3. 启动容器:

    docker run -d --name node-14-buster -v /code:/code node:14-buster sh -c 'cd code && npm start'
    docker run -d --name node-14-buster-slim -v /code:/code node:14-buster sh -c 'cd code && npm start'
    docker run -d --name node-14-alpine -v /code:/code node:14-alpine sh -c 'cd code && npm start'
    
  4. 重启容器

    在重启 buster/buster-slim 镜像的时候,发现速度很慢,达到了 10s 的超时时间,那是因为没有响应 docker 传递的 SIGTERM 信号,可以通过下面的代码测试出来:

    docker logs -f node-14-buster
    # 然后开新命令行
    docker kill -s SIGTERM node-14-buster
    docker restart node-14-buster
    

    buster 和 buster-slim 都无法接受 SIGTERM 信号,alpine 则可以。

自定义镜像

凑巧了,我们目前的项目既用到了 C++ 插件,又要监听 SIGTERM 事件,官方提供的三类镜像都不能用了,不过基于其他操作系统定制一个 node.js 镜像也很容易,例如基于 Centos7 系统创建 Dockerfile 如下:

FROM centos:7
RUN curl -L https://dl.yarnpkg.com/rpm/yarn.repo -o /etc/yum.repos.d/yarn.repo
RUN curl --silent --location https://rpm.nodesource.com/setup_14.x | bash -
RUN yum install -y nodejs yarn
WORKDIR /code
EXPOSE 80
CMD npm start

然后构建 node.js 镜像:

docker build -t node:14-centos7 .