本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2021年01月10日 统计字数: 5099字 阅读时间: 11分钟阅读 本文链接: https://soulteary.com/2021/01/10/let-us-start-with-the-mirroring-of-the-nginx-njs-tool-package.html ----- # 从封装 Nginx NJS 工具镜像聊起 最近发现有不少需求可以通过 Nginx JavaScript (NJS)来完成,相比较运行一套完整的 Web 服务来说,轻量高效的方案总是惹人喜爱,更何况这套方案是由 Nginx 官方团队推出,并搭上了繁荣的 JavaScript 生态。 本篇文章先从 NJS 容器封装、以及容器镜像优化来聊聊。 ## 写在前面 NJS 目前还处于相对早期的版本,截止本篇文章发布,官方最新的版本是 0.5.0,官网并没有二进制文件可以下载,软件随 Nginx 应用的各版本软件包提供,目前并未独立提供。 不过为了更方便的进行脚本调试,能够使用显式声明的使用 NJS 的运行时,我创建了一个开源项目,包含了 NJS 目前的主要版本的容器镜像:[https://github.com/soulteary/docker-njs](https://github.com/soulteary/docker-njs)。 相比较官方镜像动辄 20MB 来说,最小的版本不到 1MB,更小的尺寸带来的是更轻量和快速的体验。如果你想获取最新的镜像,可以访问 [DockerHub 官方仓库](https://hub.docker.com/r/soulteary/docker-njs/tags?page=1&ordering=name)。 下面来聊聊如何针对 NJS 进行镜像封装以及过程中的一些思考。 ## 基于官方镜像进行镜像构建 构建 NJS 镜像的最简单的方式是从官方容器中直接提取我们所需要的可执行文件。况且,相对于自行编译,官方构建产物更让人用的放心一些。 通过分析发现 NJS 依赖 `libpcre`、`libedit`、`libncursesw`,所以除了将 `njs` 的 `bin` 文件提取之外,还需要将上述依赖库进行拷贝 。 以最新版本的 NJS 封装为例: ```bash FROM nginx:1.19.6-alpine AS builder FROM alpine:3.12 COPY --from=builder /usr/bin/njs /usr/bin/njs COPY --from=builder /usr/lib/libpcre.so.1.2.12 \ /usr/lib/libedit.so.0.0.63 \ /usr/lib/libncursesw.so.6.2 \ /usr/lib/ RUN ln -sf /usr/lib/libpcre.so.1.2.12 /usr/lib/libpcre.so.1 && \ ln -sf /usr/lib/libedit.so.0.0.63 /usr/lib/libedit.so.0 && \ ln -sf /usr/lib/libncursesw.so.6.2 /usr/lib/libncursesw.so.6 ENTRYPOINT [ "njs" ] ``` 这样一个基础的 NJS 镜像就构建好了。 ### 针对不同版本进行构建 常常使用容器的小伙伴都知道 Nginx 官方提供了 Alpine / Debian 两个版本的镜像,而 NJS 目前也有三个小版本:0.3.x / 0.4.x 以及最新的 0.5.x,而这几个版本对于上述依赖库的版本、以及基础 Nginx 依赖都略有不同。 为了减少代码重复,以及提高代码可维护性,可以将不同版本的依赖单独声明为 `.env` 配置文件,然后搭配一个抽象度比较高的容器配置文件,对多个版本进行构建。以0.5.0 的 NJS 为例: ```bash DIST_OS=debian:10 NGX_VER=1.19.6 PCRE_VER=1.2.12 EDIT_VER=0.0.63 CURSESW_VER=6.2 ``` 将上面的文件保存为 `.env` 保存至 `njs/0.5.0/.env`,接着开始编写 Dockerfile: ```bash ARG DIST_OS=alpine:3.12 ARG NGX_VER=1.19.6-alpine FROM "nginx:$NGX_VER" AS builder FROM "$DIST_OS" COPY --from=builder /usr/bin/njs /usr/bin/njs COPY --from=builder /usr/lib/libpcre.so.* \ /usr/lib/libedit.so.* \ /usr/lib/libncursesw.so.* \ /usr/lib/ RUN ls /usr/lib/libpcre.so.*.* | xargs -I {} ln -sf {} $(echo {} | cut -b 1-21) && \ ls /usr/lib/libedit.so.*.* | xargs -I {} ln -sf {} $(echo {} | cut -b 1-21) && \ ls /usr/lib/libncursesw.so.*.* | xargs -I {} ln -sf {} $(echo {} | cut -b 1-25) ENTRYPOINT [ "njs" ] ``` 其他几个版本也可以如法炮制,最终整个项目结构如下: ```bash ├── Dockerfile ├── LICENSE ├── README.md ├── docker-build.sh ├── docker-slim.sh └── njs    ├── 0.3.9    ├── 0.3.9-alpine    ├── 0.4.4    ├── 0.4.4-alpine    ├── 0.5.0    └── 0.5.0-alpine ``` 为了能够自动化的构建各个版本的 NJS 镜像,我们需要编写一个 BASH 脚本: ```bash #!/bin/bash RELEASE_DIR='./njs'; REPO_NAME='soulteary/docker-njs' for njs_ver in $RELEASE_DIR/*; do tag=$(echo $njs_ver | cut -b 7-); echo "Build: $tag"; set -a . "$njs_ver/.env" set +a docker build --build-arg DIST_OS=$DIST_OS --build-arg NGX_VER=$NGX_VER --build-arg PCRE_VER=$PCRE_VER --build-arg EDIT_VER=$EDIT_VER --build-arg CURSESW_VER=$CURSESW_VER --tag $REPO_NAME:$tag . done ``` 将上面的内容保存为 make-image.sh,然后执行它之后就能得到各个版本的镜像了。 ```TeXT REPOSITORY TAG IMAGE ID CREATED SIZE njs 0.5.0-alpine 0f6e379160a1 About a minute ago 8.07MB njs 0.5.0 80071066f7ca About a minute ago 115MB njs 0.4.4-alpine 5b9fb7872be3 About a minute ago 8.06MB njs 0.4.4 d6663992a6ec About a minute ago 115MB njs 0.3.9-alpine 155e2a710c02 About a minute ago 7.97MB njs 0.3.9 7a041ccd4f86 About a minute ago 101MB ``` ### 使用 docker-slim 优化镜像尺寸 上文构建完毕的镜像尺寸略大了一些,可以借助 Docker Slim 进行精简。下载[Docker Slim](https://github.com/docker-slim/docker-slim) 后,使用命令对原有镜像进行二次构建,即可构建出新的小巧的镜像: ```bash docker-slim build --target soulteary/njs:0.5.0 --tag soulteary/njs:0.5.0-slim --http-probe=false ``` 为了减少后续维护成本,我们可以和之前构建不同版本 NJS 一样,准备一个 `slim.sh` 脚本,简化后续操作: ```bash #!/bin/bash RELEASE_DIR='./njs'; REPO_NAME='soulteary/docker-njs' for njs_ver in $RELEASE_DIR/*; do tag=$(echo $njs_ver | cut -b 7-); echo "Build: $tag"; set -a . "$njs_ver/.env" set +a docker-slim build --target $REPO_NAME:$tag --tag $REPO_NAME:$tag-slim --http-probe=false done ``` 脚本执行完毕,可以看到本地镜像尺寸有了大幅的减少,如果我们推送到 DockerHub,官方镜像仓库会对镜像进一步压缩,最终最小的镜像尺寸会在 1MB 以内,非常利于快速启动,进行调试。 ```TeXT REPOSITORY TAG IMAGE ID CREATED SIZE njs 0.5.0-alpine-slim 9cd49bb22a26 About a minute ago 2.17MB njs 0.5.0-slim 766a3f6ef92b About a minute ago 2.96MB njs 0.4.4-alpine-slim 2f401dee2bd6 About a minute ago 2.16MB njs 0.4.4-slim d62a8af54253 About a minute ago 2.96MB njs 0.3.9-alpine-slim 5dc7a6799f66 About a minute ago 2.03MB njs 0.3.9-slim 32b0ec660c4b About a minute ago 2.28MB ``` ## 生成批量推送镜像脚本 一次性生成多个镜像之后,如果是手动推送到 DockerHub 其实挺繁琐的,这个时候可以使用[Docker Image 命令](https://docs.docker.com/engine/reference/commandline/images/) 来“偷懒”: ```bash docker images soulteary/docker-njs --format "docker push {{.Repository}}:{{.Tag}}" ``` 使用格式参数,可以快速生成带 `docker push` 的命令,然后不论是在命令行中通过管道符执行,还是保存为文件执行,都可以做到批量推送镜像啦: ```TeXT docker push soulteary/docker-njs:0.5.0-alpine-slim docker push soulteary/docker-njs:0.5.0-slim docker push soulteary/docker-njs:0.5.0-alpine docker push soulteary/docker-njs:0.5.0 ``` ## 其他 Nginx 在去年十二月发布了主线版本的例行更新,版本升级到了 [1.19.6](http://nginx.org/en/CHANGES),官方对于本次升级只有聊聊数语,未曾提到 NJS 相关的事情: ```TeXT Changes with nginx 1.19.6 15 Dec 2020 *) Bugfix: "no live upstreams" errors if a "server" inside "upstream" block was marked as "down". *) Bugfix: a segmentation fault might occur in a worker process if HTTPS was used; the bug had appeared in 1.19.5. *) Bugfix: nginx returned the 400 response on requests like "GET http://example.com?args HTTP/1.0". *) Bugfix: in the ngx_http_flv_module and ngx_http_mp4_module. Thanks to Chris Newton. ``` 但是因为折腾 NJS 运行时镜像,发现了这个隐藏在主版本更新日志外的变更,发现了这个时隔三个月[大量更新](https://nginx.org/en/docs/njs/changes.html)的 0.5.0。 ## 最后 我创建了一个名为 [njs-learning-materials](https://github.com/soulteary/njs-learning-materials) 的开源仓库,目前已经整理了 NJS 相关的一些开源参考资料,后续会随着更深入的折腾,不断更新和补充内容。 如果你感兴趣的话,欢迎加入我一起折腾。 --EOF