ThinkJS 项目构建 Docker 镜像

提醒:本文最后更新于 2152 天前,文中所描述的信息可能已发生改变,请谨慎使用。

其实这个话题很简单,不是很想写这篇文章。不过的确还是有很多朋友在打包构建部署上存在一些问题,恰巧最近使用 Docker 部署了几个 ThinkJS 相关的项目,所以还是拿出来说说吧。需要提前说明的是本文并不是 Docker 的基础教程,默认大家都是了解 Docker 的。然后我会分享一下我觉得 ThinkJS 项目构建和部署过程中可能需要注意的点,我们先说说如何构建镜像,然后再说一下可能出现的问题。

构建镜像

基础镜像

FROM mhart/alpine-node:8.9.4

首先推荐大家基于 mhart/alpine-node 的镜像来构建,因为真的很小(20M左右)!不过因为小有很多的构建工具都不是很全,如果遇上一些安装时需要编译的模块时可能会缺少环境,这个时候你可以选择手动去安装环境,当然也可以选择使用 Docker 官方的 library/node 镜像(800M左右),为了省事推荐这种情况下还是使用后者比较好。

依赖安装

COPY package.json /animaris/package.json
RUN npm i --production --registry=https://registry.npm.taobao.org

选好了基础镜像之后下面就需要拷贝项目文件了。这里推荐大家先把 packge.json 文件 COPY 进来然后安装依赖。因为依赖的变化比较小,可以认为一段时间内这部分的镜像层都是稳定的。而项目代码会随着需求变更而经常变化。综上所述,我们选择让不变的镜像层优先打包保证镜像层的最大可复用性。

拷贝项目

一个正常的 ThinkJS 项目其实线上运行只需要以下几个文件:

  • app/:项目原始码文件夹,如果是未编译项目的话对应的是 src/ 目录
  • view/:前端模板文件夹
  • www/:前端静态资源文件夹
  • production.js:ThinkJS 启动脚本

所以只要按照顺序把这些文件 COPY 进去就可以了。

启动项目

大家平常都习惯使用 PM2 来启动 NodeJS 项目,它最大的好处就是能够帮我们守护项目进程,当进程被杀死的时候能帮我们自动重启。不过 Docker 本身就是有自动重启的特性的,所以在很多层面上 Docker 和 PM2 一块使用都有点冲突。其实因为 Docker 充当了守护的角色,我们完全可以直接使用 node production.js 命令去启动。以下是一个完整的 ThinkJS 项目 Dockerfile 示例:

FROM mhart/alpine-node:8.9.4

WORKDIR /animaris
COPY package.json /animaris/package.json
RUN npm i --production --registry=https://registry.npm.taobao.org

COPY src /animaris/src
COPY view /animaris/view
COPY www /animaris/www
COPY production.js /animaris/production.js

ENV DOCKER=true
EXPOSE 8360
CMD [ "node", "/animaris/production.js" ]

使用如下命令进行构建:

docker build -t lizheming/animaris ./Dockerfile

之后使用如下命令运行镜像,即可使用 http://localhost:8360 访问网站:

docker run -p 8360:8360 lizheming/animaris

注意事项

本地文件

使用 Docker 一定不能忘记的特性就是容器销毁后容器内的所有资源都是会被销毁的,下回会重新初始化,所以如果是需要持久化保存的应避免写到容器中,需要选择外部的持久化存储,例如 Volume 共享卷或者 S3 等相关服务。

ThinkJS 项目启动后一般会有 logs, runtime 两个目录会写入文件。其中 logs 是用来存储线上日志用的,这个最好使用共享卷的形式外载出来,方便之后排查问题。runtime 这个是 ThinkJS 运行时临时文件的存储地方,例如 cache 和 session 等。session 会话默认是使用文件类型存储的,如果使用 Docker 的话推荐选择 MySQL 等外部存储。其它的功能有相关需求也可以参考 session 服务。当然如果懒得用直接把 runtime 共享卷出来也是可以的。

还有就是如果有用户上传类的需求会上传到本地文件夹上也要记得共享出来,否则丢数据就完蛋叻!

静态资源

ThinkJS 一直主张在生产环境中使用 Nginx 来处理静态资源,这样不需要经过 Node 层直接 Server 转发性能会更高。不过这样就给镜像打包造成了一定的麻烦,因为静态资源也被打包到项目镜像里去了,而 Nginx 镜像正常是没办法跨镜像读取到文件的,所以就死解了。

在 ThinkJS 中是利用 think-resource 这个中间件来处理静态资源的访问的,然后它在线上环境的状态是 enable: false。其实静态资源过一层 Node 并消耗不了多少资源,除非对性能有严苛要求的,我建议都可以直接把这个功能打开。这样所有的请求都统一成 Nginx 转发到 Node 镜像,解决了 Nginx 需要跨镜像读取文件的问题。具体可参考官方文档的“为什么上线后静态资源访问不了?

module.exports = [
  ...
  {
    handle: 'resource',
    enable: true // 始终开启,默认为 `enable: isDev` 表示只再开发环境下开启
  },
  ...
]

当然如果真的要在打包成一个镜像的情况下用 Nginx 处理静态资源也不是没有办法,我们可以利用共享卷来操作。Node 镜像通过将镜像文件共享卷的形式映射到本地,然后 Nginx 镜像通过共享卷的形式将之前本地映射的文件再映射到镜像中。这样通过本地的一层转发即可实现 Nginx 的跨镜像访问静态资源。

# ThinkJS 镜像
docker run \
    -v ./www:/app/www \
    -p 8360:8360 \
lizheming/animaris

# Nginx 镜像
docker run \
    -v ./nginx.conf:/etc/nginx/conf.d/animaris.conf \
    -v ./www:/var/www/animaris/www \
    -p 80:80 \
nginx
server {
    listen 80;
    server_name animaris.eming.li;
    root /var/www/animaris/www;

    location ~ /static/ {
        etag         on;
        expires      max;
    }
}

环境变量

平常我们是使用 development.jsproduction.js 来区分开发环境和生产环境的,然鹅这么打包之后发现都是生产环境了。这样有时候我们需要不同环境不同配置就不太好弄了。这里推荐大家使用环境变量来区分。

think.env = process.ENV.ENV;
docker run -e ENV=test -p 8360:8360 lizheming/animaris

终端日志

有用户提出疑问 #1106,说“网站500了为啥 logs 里没看到日志?”。这是因为虽然 ThinkJS 内部的日志都是用 think-logger 模块处理了,但是因为跨模块或者启动时机的问题,有一些日志没办法走日志模块记录并存储到文件,会直接打到终端里。所以当没有日志的时候我们可以考虑去 docker logs 中捞一下终端的日志,说不定有意外的惊喜。

后记

虽然文字写了很多,除了一些普适问题需要考虑一下之外,其实整体来看整个构建过程还是很简单的。同时也欢迎大家有什么问题和疑问留言交流。

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

2 评论

luckyscript Chrome73.0 Mac OS 10.13.6
2019-04-12 16:32:27 回复

在用mysql这些镜像的时候,感觉坑还蛮多的。compose的时候,mysql有时候启的比node应用慢(因为可能要刷库,导致node应用启动报错。后来我用了 github上 wait-for-it.sh 来确保他俩顺序执行。但是用的时候发现wait-for-it.sh里面的语法是bash的而alpine-node所用的精简版linux只有最基本的sh。然后我又不得不给这个精简版Linux安装bash。折腾到最后感觉没啥问题了,但是最后跑的时候发现,我线上的服务器带不动docker。???

公子 Chrome74.0 Mac OS 10.14.4
2019-05-12 05:30:52 回复

@luckyscript 尴尬,你这条评论居然被识别成垃圾评论了,这会儿才看到。的确是会有这种情况出现,有些容器要初始化的过程很长,但是的确是启动了所有 Docker 这块识别不出。一般来说我都是简单的加个 sleep 来解决的。alpine 那个镜像的确坑还蛮多的,啥都没有,自用的话我一般还是会用 Debian 来构建。线上服务器最好 2G 内存以上吧,要不然真的很容易崩,不要问我为啥知道…