如何选择 Node.js 的 Docker 镜像

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 版本,分别下载上述三类镜像进行对比:

1
2
3
docker pull node:14-buster
docker pull node:14-buster-slim
docker pull node:14-alpine

镜像体积大小对比

运行 docker images | grep node

1
2
3
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 则更为轻巧。

容器内存占用对比

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

1
2
3
4
5
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 查看运行时的内存占用

1
2
3
4
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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    console.log('pid', process.pid)
    process.on('beforeExit', (code) => {
    console.log('进程 beforeExit 事件的代码: ', code)
    })
    process.on('exit', (code) => {
    console.log('进程 exit 事件的代码: ', code)
    })
    process.on('SIGTERM', (code) => {
    console.log('SIGTERM', code)
    process.exit(0)
    })
    process.on('SIGINT', (code) => {
    console.log('SIGINT', code)
    process.exit(0)
    })
    require('http')
    .createServer((req, res) => res.end('Hello World'))
    .listen(3030)
  2. 然后创建 package.json :

    1
    2
    3
    4
    5
    {
    "scripts": {
    "start": "node index.js"
    }
    }
  3. 启动容器:

    1
    2
    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 信号,可以通过下面的代码测试出来:

    1
    2
    3
    4
    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 定制的 node.js 镜像:

Dockerfile 如下:

1
2
3
4
5
6
7
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

生成镜像:

1
docker build -t node:14-centos7 .

该镜像既可以运行 C++ 插件,又能接收 Docker 的 SIGTERM 事件。