最近发现有不少需求可以通过 Nginx JavaScript (NJS)来完成,相比较运行一套完整的 Web 服务来说,轻量高效的方案总是惹人喜爱,更何况这套方案是由 Nginx 官方团队推出,并搭上了繁荣的 JavaScript 生态。

本篇文章先从 NJS 容器封装、以及容器镜像优化来聊聊。

写在前面

NJS 目前还处于相对早期的版本,截止本篇文章发布,官方最新的版本是 0.5.0,官网并没有二进制文件可以下载,软件随 Nginx 应用的各版本软件包提供,目前并未独立提供。

不过为了更方便的进行脚本调试,能够使用显式声明的使用 NJS 的运行时,我创建了一个开源项目,包含了 NJS 目前的主要版本的容器镜像:https://github.com/soulteary/docker-njs

相比较官方镜像动辄 20MB 来说,最小的版本不到 1MB,更小的尺寸带来的是更轻量和快速的体验。如果你想获取最新的镜像,可以访问 DockerHub 官方仓库

下面来聊聊如何针对 NJS 进行镜像封装以及过程中的一些思考。

基于官方镜像进行镜像构建

构建 NJS 镜像的最简单的方式是从官方容器中直接提取我们所需要的可执行文件。况且,相对于自行编译,官方构建产物更让人用的放心一些。

通过分析发现 NJS 依赖 libpcrelibeditlibncursesw,所以除了将 njsbin 文件提取之外,还需要将上述依赖库进行拷贝 。

以最新版本的 NJS 封装为例:

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 为例:

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:

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" ]

其他几个版本也可以如法炮制,最终整个项目结构如下:

├── 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 脚本:

#!/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,然后执行它之后就能得到各个版本的镜像了。

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 后,使用命令对原有镜像进行二次构建,即可构建出新的小巧的镜像:

docker-slim build --target soulteary/njs:0.5.0 --tag soulteary/njs:0.5.0-slim --http-probe=false

为了减少后续维护成本,我们可以和之前构建不同版本 NJS 一样,准备一个 slim.sh 脚本,简化后续操作:

#!/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 以内,非常利于快速启动,进行调试。

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 命令 来“偷懒”:

docker images soulteary/docker-njs --format "docker push {{.Repository}}:{{.Tag}}"

使用格式参数,可以快速生成带 docker push 的命令,然后不论是在命令行中通过管道符执行,还是保存为文件执行,都可以做到批量推送镜像啦:

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,官方对于本次升级只有聊聊数语,未曾提到 NJS 相关的事情:

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 运行时镜像,发现了这个隐藏在主版本更新日志外的变更,发现了这个时隔三个月大量更新的 0.5.0。

最后

我创建了一个名为 njs-learning-materials 的开源仓库,目前已经整理了 NJS 相关的一些开源参考资料,后续会随着更深入的折腾,不断更新和补充内容。

如果你感兴趣的话,欢迎加入我一起折腾。

–EOF