通过Docker部署Nodejs项目产生的一些思考

最近遇到动态页面数据抓取的需求,所以有在研究 Puppeteer 的使用方法。不过之前没有接触过 Nodejs,所以也是通过网上的几篇教程在不断的摸索和测试中一点点了解。

也可以说是自己习惯了但凡遇到一个项目都想要把它Docker容器化来便于后续的操作,也就顺便研究了一下Nodejs项目的Docker化部署。

期间一些小的思考,随手记录一下。

第一版Dockerfile

从网上搜索了一些通过docker部署nodejs项目的文章,其中大部分的 Dockerfile 文件写法都如下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FROM node:11-alpine
LABEL maintainer="leafney <babycoolzx@126.com>"
# 设置国内阿里云镜像站、安装chromium 68、文泉驿免费中文字体等依赖库
RUN echo "https://mirrors.aliyun.com/alpine/v3.9/main/" > /etc/apk/repositories && \
echo "https://mirrors.aliyun.com/alpine/v3.9/community/" >> /etc/apk/repositories && \
echo "https://mirrors.aliyun.com/alpine/edge/testing/" >> /etc/apk/repositories && \
apk -U --no-cache update && \
apk -U --no-cache --allow-untrusted add tzdata chromium ttf-freefont wqy-zenhei ca-certificates && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
mkdir -p /logs && \
rm -rf /var/cache/apk/*
WORKDIR /app
# 复制宿主机当前路径下所有文件到docker的工作目录
COPY . /app
RUN npm config set registry 'https://registry.npm.taobao.org' && \
yarn config set registry 'https://registry.npm.taobao.org' && \
yarn global add pm2 && \
yarn install && \
yarn cache clean
VOLUME ["/logs"]
# Start pm2.json process file
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

一般的项目目录结构如下:

1
2
3
4
5
6
|-- node_app
-- src
-- index.js
-- package.json
-- ecosystem.config.js
-- Dockerfile

按照上面的 Dockerfile 文件操作步骤,其中关键的一步:

1
2
# 复制宿主机当前路径下所有文件到docker的工作目录
COPY . /app

表示将 node_app 项目目录下的所有文件拷贝到了Docker镜像中,然后执行 yarn install 来安装所有依赖包,最后通过 pm2 启动项目。之后基于该镜像来构建容器直接运行即可。

那么问题来了,当我的项目代码有更新后需要重新上线新版本时,要如何操作呢?

按照上面的 Dockerfile ,操作步骤应该是:

  1. 停止容器,删除容器
  2. 从git拉取最新项目代码
  3. 重新 docker build 镜像,同时把代码打包到镜像中
  4. 通过新镜像创建容器,并启动容器运行

过程似乎很简单,但细想一下,每次一有代码更新,都需要重新来构建镜像。那如果每次的更新都是一些比较小的改动呢?相比之下重新构建镜像所耗费的时间就比较长了。

更严重的是,每一次重新构建镜像,之前可用的镜像就直接作废了,如果不及时删除,会占用很大的硬盘空间。

基于以上诸多的问题,我们来考虑如何优化?


第二版Dockerfile

那么,在我看来,理想状态的操作应该是这样的:

  1. 停止容器
  2. 从git拉取最新项目代码
  3. 重启容器

That’t all! 就是这么简单。

操作非常的简单明了,那看一下如何修改上面第一版的 Dockerfile 文件吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM node:11-alpine
LABEL maintainer="leafney <babycoolzx@126.com>"
# 在国内由于网络原因,软件下载比较慢,所以加入了国内的软件源以加速构建
# 设置国内阿里云镜像站,安装chromium、文泉驿免费中文字体等依赖库,配置npm和yarn的taobao仓库
RUN echo "https://mirrors.aliyun.com/alpine/v3.9/main/" > /etc/apk/repositories && \
echo "https://mirrors.aliyun.com/alpine/v3.9/community/" >> /etc/apk/repositories && \
echo "https://mirrors.aliyun.com/alpine/edge/testing/" >> /etc/apk/repositories && \
apk add -U --no-cache --allow-untrusted tzdata chromium ttf-freefont wqy-zenhei ca-certificates && \
mkdir -p /app /logs && \
npm config set registry 'https://registry.npm.taobao.org' && \
yarn config set registry 'https://registry.npm.taobao.org' && \
yarn global add pm2 && \
yarn cache clean && \
rm -rf /var/cache/apk/*
COPY ./startup.sh /usr/local/bin/
RUN chmod +x usr/local/bin/startup.sh
WORKDIR /app
VOLUME ["/app"]
EXPOSE 8000
CMD [ "startup.sh" ]

startup.sh :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
set -e
WDir=/app
if [ "$(ls -A ${WDir})" ]; then
echo "[i] ***** dir /app have files,so start init. *****"
echo "[i] ***** yarn install *****"
yarn install
echo "[i] ***** pm2 start *****"
pm2-runtime start ecosystem.config.js
else
echo "[e] ***** dir /app is empty,so can not run. *****"
echo "[i] ***** Please copy project files to VOLUME for /app and then restart docker container *****"
node
fi

总体来说,改动的思路就是:将不需要变化的那些操作打包进docker镜像中,而需要经常改动的操作如 更新项目代码,执行 yarn install 安装新的依赖包等,都放到 startup.sh 中。而对于更复杂的操作,后续可以基于该镜像直接做扩展。

另外,这里使用 yarn install 而没有用 npm install 的一个优势是 如果你的 package.json 文件在更新时没有变化,yarn install 也只会执行一次,后续的操作都是读取cache缓存了,所以后续的代码更新操作也就非常快速了。

具体关于 yarnnpm 的区别,这里不再详细展开说明。


进一步优化

上面的操作虽说已经很简便了,但每次更新代码的时候还是需要重启一下容器才能应用上最新的改动。那还有没有优化的空间呢?

答案是 当然。

一般的Nodejs项目我们可以通过 pm2 来管理和监控进程,保证node进程持续运行或崩溃时自动重启。而 pm2watch 参数可以监听应用目录的变化,一旦发生变化,就会自动重启。我们可以利用这个功能来实现改动代码的自动更新效果。

pm2 的配置文件中增加如下代码:

1
2
3
4
{
"watch": ["server", "client"],
"ignore_watch" : ["node_modules", "client/img"],
}

watch 参数默认情况下为 false ,可以设置需要监听改动的文件或目录。在 ignore_watch 参数中设置排除监听改动的文件或目录。

这样,只要监听到代码有改动,pm2 就会自动重启。对于项目更新的操作我们需要做的也就只有一项 从git拉取最新项目代码 而已了。


总结

其实无论是第一种的每次生成镜像还是第三种更加灵活的自动重启,具体如何应用还是需要看所对应的需求而言的,并不能一味的评判孰优孰劣。

我实现的Docker镜像地址:leafney/alpine-nodejs - Docker Hub

简单的实现了一个Nodejs项目的Docker镜像,如有考虑不到的地方,欢迎大家提出建议和讨论。


相关参考

坚持原创技术分享,您的支持将鼓励我继续创作!
如有疑问或需要技术讨论,请留言或发邮件到 service@itfanr.cc