制作Node项目的docker镜像

最近一个项目需要在服务端用到无头浏览器puppeteer,这个工具对于node版本有一定要求,而我们目前的线上node镜像是8.9版本的,无法支持该无头浏览器,因此需要升级node,重新制作镜像。

之前的node镜像的维护人员已经找不到了,因此这次我自己制作,把过程记录下来,供下次升级node作为参考。

阅读官方文档

官方文档永远是最好的文档,因此每个软件安装前,都务必看下官方文档,了解其对于环境和软件的依赖。

比如Node的文档

拉取基础镜像

因为一些原因,我们只能使用CentOS6.9版本的镜像作为基础镜像。

1
docker pull centos:6.9

安装工具和依赖库

更换yum源

1
2
3
4
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
curl "https://mirrors.aliyun.com/repo/Centos-6.repo" -o /etc/yum.repos.d/Centos-6.repo
yum clean all
yum makecache

安装依赖库

1
yum install pcre-devel perl openssl-devel zlib-devel telnet wget
名称 依赖源
perl OpenResty
pcre-devel OpenResty
zlib-devel Node

升级Python

1
2
3
4
5
6
7
8
tar -xvf Python-2.7.18.tar
cd Python-2.7.18
# 务必加上--with-zlib,否则后面编译node会失败
./configure --prefix=/usr/local/python27 --with-zlib
make
make install
mv /usr/bin/python /usr/bin/python2.6.6
ln -s /usr/local/python27/bin/python2.7 /usr/bin/python

注意:Python 软链接指向 Python2.7 版本后,因为yum是不兼容 Python 2.7的,所以yum不能正常工作,我们需要指定 yum 的Python版本:

编辑/usr/bin/yum文件,将头部的#!/usr/bin/python改成#!/usr/bin/python2.6.6

升级GCC

可以参考这个文章

1
2
3
4
yum install gcc gcc-c++ -y
yum install -y centos-release-scl
yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable

安装Node

官网下载源码,然后根据下方的安装指南操作即可。

1
2
3
4
5
6
7
8
9
node-v12.18.3.tar.gz 
cd node-v12.18.3

./configure --prefix=/usr/local/node
make -j4
make install

ln -s /usr/local/node/bin/node /usr/bin/node
ln -s /usr/local/node/bin/npm /usr/bin/npm

注意:Node的make -j4耗时非常久,我在docker内部编译花了几十分钟,请提前预留好时间。

安装OpenResty

官网下载源码,然后根据下方的安装指南操作即可。

1
2
3
4
5
6
7
tar -xvzf openresty-1.17.8.2.tar.gz 
cd openresty-1.17.8.2

./configure --prefix=/usr/local/openresty
gmake
gmake install
useradd www

进程管理

是通过s6这个进程管理工具来实现的。包括开机启动项、容器启动时需要执行的一些文件操作等。

具体的内容可以查看我的docker-node这个git项目(script下面的脚本应该是用不着的,只需查看S6即可)。

里面有2个目录需要说明下:

etc/services.d:配置开机启动的命令,比如启动nginx。

etc/cont-init.d:配置开机时的文件操作,比如将项目下的nginx配置文件移动到/usr/local/openresty/nginx/conf/vhosts目录下。

配置项规则

我们制定了一个规则(该规则源自全公司的PHP镜像规则):

项目相关的服务器配置信息,统一存放在项目下的.builddep目录下。

镜像里面会自动根据环境变量自动读取对应的项目配置信息,并copy到指定目录的功能。

设置时区

1
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

清理无用文件

安装完成后,记得把各个安装包都删除掉,以减小镜像体积。

卸载无用的工具软件以及缓存数据

安装好上述软件后,我生成的镜像有1.16G,太大了,因此我需要将无用的软件删除掉,重新生成镜像。清理的内容包括:

  • yum缓存,可以看到/var/cache/yum这个目录有237M的数据,可以通过yum clean all清理掉。

  • python,在/usr/local/python27下有100多M,我把这个目录删除了,然后将/usr/bin/python还原为系统默认的2.6.6版本。

这样再次生成的镜像就降低到800M了,不过还是很大,后面还需要进一步精简。

将容器保存为镜像

1
docker commit 容器ID 镜像名称

到了这一步,镜像制作就完成了。

接下去就是运行我们的项目程序了。

推送到dockerhub

需要先在dockerhub注册好账号,并提前创建好镜像仓库。另外docker镜像的命名必须符合规范,即docker账号ID/镜像仓库|tag的格式:

1
2
3
docker login
docker tag node:12.18.3 zhouchangju/node:12.18.3
docker push zhouchangju/node:12.18.3

制作项目镜像

(TODO)编写项目的Dockerfile

为了简化发布操作,我们需要额外再编写一个项目镜像的Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Be used in my local development environment
FROM node-12.18.3:latest

WORKDIR /var/www/resource
RUN npm config set registry 'https://registry.npm.taobao.org'

RUN mkdir -p /tmp/server
COPY server/package.json /tmp/server/package.json
RUN cd /tmp/server && npm install --ignore-scripts
RUN mkdir -p /var/www/resource/server && cp -a /tmp/server/node_modules /var/www/resource/server/

RUN mkdir -p /var/www/resource/front

COPY ./ /var/www/resource/

ENV CODEPATH='/var/www/resource/'

EXPOSE 7001
EXPOSE 80

生成镜像

为了和线上环境的Dockerfile区分开,我这个本地测试用的文件命名成了Dockerfile.local:

1
docker build -f Dockerfile.local -t resource:local  .

启动程序

为了方便测试,我把容器的80端口也转到docker服务器的80端口了:

1
docker run -ti -p 80:80 --add-host db-resource-mysql:111.229.93.78 resource:local  /bin/bash

注意:

1、The docker command line is order sensitive. The order of args goes:
docker ${args_to_docker} run ${args_to_run} image_ref ${cmd_in_container}
所以-p设置暴露端口这部分,别放错位置了,否则会报错。

2、hosts信息的修改,是不会保存在镜像中的,因此需要每次启动的时候进行指定,即通过上面的–add-host参数指定。

至此,就可以通过docker服务器的80端口,访问docker程序的接口了。

常见报错

编译Node报错:ImportError: No module named zlib

缺少了依赖,重新安装后解决:

1
yum install zlib-devel

编译Node报错:node undefined reference to `clock_getres’

参考Node的这个issue:

https://github.com/nodejs/node/issues/30077

通过设置环境变量解决了这个问题:

1
export LDFLAGS=-lrt

ENTRYPOINT指定的脚本,内部的tail -f /etc/hosts不会挂起

比如我写了这样的ENTRYPOINT:

1
ENTRYPOINT ['/bin/bash', '-l', '-c', '/etc/start.sh']

在start.sh文件的末尾执行了tail -f /etc/hosts,预期是让start.sh脚本一直挂起,但是结果发现这个脚本瞬间就退出了,执行的结果状态码也是正常的0,即exit(0)

这个和子进程机制有关系,将其改为在ENTRYPOINT中执行即可,比如:

1
ENTRYPOINT ['/bin/bash', '-l', '-c', '/etc/start.sh && tail -f /etc/hosts']

一些技巧

通过set调试shell脚本

可以参考阮一峰的博客:

http://www.ruanyifeng.com/blog/2017/11/bash-set.html

用得比较多的一般是set -e(脚本只要发生错误,就终止执行。)和set -x(在运行结果之前,先输出执行的那一行命令)。