制作Node项目的docker镜像
最近一个项目需要在服务端用到无头浏览器puppeteer,这个工具对于node版本有一定要求,而我们目前的线上node镜像是8.9版本的,无法支持该无头浏览器,因此需要升级node,重新制作镜像。
之前的node镜像的维护人员已经找不到了,因此这次我自己制作,把过程记录下来,供下次升级node作为参考。
阅读官方文档
官方文档永远是最好的文档,因此每个软件安装前,都务必看下官方文档,了解其对于环境和软件的依赖。
比如Node的文档。
拉取基础镜像
因为一些原因,我们只能使用CentOS6.9版本的镜像作为基础镜像。
docker pull centos:6.9
安装工具和依赖库
更换yum源
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
安装依赖库
yum install pcre-devel perl openssl-devel zlib-devel telnet wget
| 名称 | 依赖源 |
|---|---|
| perl | OpenResty |
| pcre-devel | OpenResty |
| zlib-devel | Node |
升级Python
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
可以参考这个文章。
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
从官网下载源码,然后根据下方的安装指南操作即可。
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
从官网下载源码,然后根据下方的安装指南操作即可。
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到指定目录的功能。
设置时区
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了,不过还是很大,后面还需要进一步精简。
将容器保存为镜像
docker commit 容器ID 镜像名称
到了这一步,镜像制作就完成了。
接下去就是运行我们的项目程序了。
推送到dockerhub
需要先在dockerhub注册好账号,并提前创建好镜像仓库。另外docker镜像的命名必须符合规范,即docker账号ID/镜像仓库|tag的格式:
docker login
docker tag node:12.18.3 zhouchangju/node:12.18.3
docker push zhouchangju/node:12.18.3
制作项目镜像
(TODO)编写项目的Dockerfile
为了简化发布操作,我们需要额外再编写一个项目镜像的Dockerfile:
# 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:
docker build -f Dockerfile.local -t resource:local .
启动程序
为了方便测试,我把容器的80端口也转到docker服务器的80端口了:
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
缺少了依赖,重新安装后解决:
yum install zlib-devel
编译Node报错:node undefined reference to `clock_getres’
参考Node的这个issue:
https://github.com/nodejs/node/issues/30077
通过设置环境变量解决了这个问题:
export LDFLAGS=-lrt
ENTRYPOINT指定的脚本,内部的tail -f /etc/hosts不会挂起
比如我写了这样的ENTRYPOINT:
ENTRYPOINT ['/bin/bash', '-l', '-c', '/etc/start.sh']
在start.sh文件的末尾执行了tail -f /etc/hosts,预期是让start.sh脚本一直挂起,但是结果发现这个脚本瞬间就退出了,执行的结果状态码也是正常的0,即exit(0)
这个和子进程机制有关系,将其改为在ENTRYPOINT中执行即可,比如:
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(在运行结果之前,先输出执行的那一行命令)。