在旧版 Nginx 官方 Dockerfile 上集成第三方模块的探索

问题背景

线上生产环境用的 nginx 1.21, 然后由于新功能引入的一个问题,需要使用第三方模块 ngx_http_subs_filter_module,目的是使用正则表达式来移除响应结果中的某些数据。

由于这个客户的环境非常重要,组内的大哥们也不敢随便升级 nginx 的版本,所以强制要求必须是用当前线上
Dokcer 正在跑的 nginx 1.21 镜像同样的 Dockerfile 来集成第三方模块后重新打包一个镜像。

这块工作难就在于需要做到最小改动,尽可能不去修改太多的地方,以免造成无法预料的影响。

文末有原版 Dockerfile
 

启程:版本调研

首先在 Dockerhub 上面找到相应的镜像主页,通过页面上的链接直接跳转到这个镜像使用的 Dockerfile
页面

https://hub.docker.com/layers/library/nginx/1.21/images/sha256-25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4?context=explore

在这里插入图片描述
 
跳转后的仓库页面锁定在了 mainline/debian 目录下面,我们将这几个文件拷贝出来进行打包

在这里插入图片描述

 

直接打包镜像

首先我们不对原有的 Dockerfile 做任何修改,直接进行打包看有没有什么问题。

打包命令:

docker build \
 --build-arg http_proxy=http://xxx:7890 \
 --build-arg https_proxy=http://xxx:7890 \
 -t test-nginx:1.0 . --no-cache 2>&1 | tee build.log

上面那条命令中我们使用了 --build-arg 这个命令行参数,他的作用是配置仅在打包运行时可见的环境变量,打包结束后不会留存在镜像中。

配置 http_proxyhttps_proxy 是为了让 docker 在打包时走我们本机的国外代理,加快依赖包的下载速度。也可以直接在国外的服务器上进行打包。
 

问题1: GPG Key获取失败

留意以下的日志,可以发现打包过程中,脚本在不断的尝试从 GPG 服务器中获取 Key, 但以失败告终。

在这里插入图片描述

在这里插入图片描述

 
我们通过观察原始的 Dockerfile, 可以在第 21:29 行找到循环获取 Key 的脚本命令。
在这里插入图片描述

 
可以看到这里 nginx 官方配置了两个服务器:

hkp://keyserver.ubuntu.com:80
pgp.mit.edu

我自己一开始也是在网上找了很多的博客,其中 80% 都是让你配置多几个备选服务器到脚本里面增大 Key 的获取成功率。但不出意外,这些方法全都解决不了这里的问题。

最终我还是回到报错中收集更多的细节信息:

首先我在 DockerHub 上面看到 1.21 镜像的 dockerfile 已经是两年前的版本了,所以其中的一些脚本或许多多少少都有些问题。

在这里插入图片描述

 
接着我们从报错日志中可以一眼看到一个警告:

Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).

在这里插入图片描述
 

这里并没有报错,报错的是从 GPG 服务器获取 Key 失败。但是我们前面探索过,加更多服务器也没用。然后这个警告是说 apt-key 已经被废弃,而我们从原 Dockerfile 里面可以找到调用这个工具的脚本:

28: apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break;

可以得知,从服务器获取 Key 的操作使用 apt-key 这个工具完成的。那么问题的定位差不多可以有个结论:

弃用了的工具 apt-key 影响了 GPG 获取 Key 的流程
 

探索

我们定位到了问题,那么自然想到的方式是升级工具,然后更新 Key 获取脚本,这里引出了三个小问题:

  1. 替换 apt-key 的工具是什么?
  2. 替换工具后的脚本要做什么改动?
  3. 替换声明是否来自官网更新文档?

这里要唠叨几句:

首先我也是初学 docker,平日里没有太多精力再去关注 dockernginx 的社区,可能有活跃的网友知道怎么改在某一个博客中提及了。但是我到目前为止没有看到问题和我这个完全一样的,所以对那些解决方案我都是持质疑态度,我个人一般是信奉官方文档和 API 定义多一些。

而且说来惭愧,我在定位到是工具问题之前,已经是花了一整天来搜索各种博客的解决方案。但最终没有一个能完美解决我的问题,一天就这么浪费了。由于这个集成方案的探索在下一周就要部署到客户现场,所以浪费的这一天也让我从这里开始到整个问题探索结束,都不会再去看网上的那些博客。同时我也意识到我这个问题应该网上也不会有很好的解决方案。

基于此,我选择直接去问官方人员,他们最清楚该改哪里。幸运的是,我在 nginxgithub 上提了 issue 后,官方人员第二天就回复我了,这里必须点一个大大的赞。而且官方人员明确指出了我用的 Dockerfile 太老旧了,他们早就替换了新版的脚本,并给出 commit 给我去参考,真的感谢。

在这里插入图片描述

 
改动的 commit:
https://github.com/nginxinc/docker-nginx/commit/38e2690b304b8dca4848f3e70a1fc95837f61510

在管理员提供的 commit 中, 他们把请求 Key 的工具从 apt-key 换成了 gpg1, 并对原始的 Dockerfile 进行了一些修改,我们照葫芦画瓢就行。

在这里插入图片描述

得益于管理员的帮助,问题一完美解决!
 

插曲, 暂时对 Dokcerfile 进行分层加速调试

学习过 DockerfileRUN 命令就知道,每个 RUN 命令都会建立一个缓存层,这样在执行完一条
RUN 命令后,只要不修改其之前和自己的脚本命令,下次执行时就不用再次等待执行。

而在网络上的大多数官方镜像的 Dockerfile 中, 我们会发现 Dockerfile 中往往只有一条 RUN
命令。这是因为为了建立缓存关系,每条 RUN 都会在当前缓存层中加东西,这样会增加每个缓存层的大小,
使得最终打包出来的镜像的大小也很大。这是非常不利于官方镜像的传输的,尤其是一些基础服务的镜像。试想
若是一个简单的服务镜像就要 7 个 G, 还会有用户愿意去使用吗?

但在本问题的讨论中,我们是要对nginx官方的 Dockerfile 进行一个 min(max(Dockerfile)) 的操作(哈哈我觉得用函数来说明更贴切,在最小改动基础上最大幅度改动),这是一个不断试错的过程,可能看我博客里面写运行一条
cmd 得到了下图结果。但是在获得这个结果截图之前,我其实是在不断尝试错误的指令。

那么为了减少时间的浪费,我们先分析原有的 RUN, 看看能在哪些地方拆开,避免重复执行一些步骤。
 

将获取 Key 的指令独立成一条 RUN

我们看下面从改动过可以正常获取 Key 的 Dockerfile 中观察到的三条脚本:

NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; # 24 行
NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg; # 25 行
gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" ; # 36 行

可以发现这里是设置了两个环境变量,一个是 GPGKEY 的值,一个是 Key 的路径。
最后一条脚本是传入 Key 值给工具 gpg1 然后导出内容到指定的路径中,至于什么内容这里不关心。
可以发现这里的产物最终放在了指定的一个目录中,且和下面其他脚本的运行没有太多显示的交集,那么我们
就可以把这块逻辑分离成一个 RUN, 最终经过一次改动的脚本摘要如下:

#
# NOTE: THIS DOCKERFILE IS GENERATED VIA "update.sh"
#
# PLEASE DO NOT EDIT IT DIRECTLY.
#
FROM debian:bullseye-slim

LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

ENV NGINX_VERSION   1.21.6
ENV NJS_VERSION     0.7.6
ENV PKG_RELEASE     1~bullseye

RUN set -x \
# create nginx user/group first, to be consistent throughout docker variants
    && addgroup --system --gid 101 nginx \
    && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \ 
    && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
    NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg; \
    export GNUPGHOME="$(mktemp -d)"; \
    found=''; \
    for server in \
        hkp://keyserver.ubuntu.com:80 \
        pgp.mit.edu \
    ; do \
        echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
        gpg1 --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
    done; \
    test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
    gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" ; \
    rm -rf "$GNUPGHOME"; \
    apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/*
RUN NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg; \
    dpkgArch="$(dpkg --print-architecture)" \
    && nginxPackages=" \
        nginx=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \
    " \
    && case "$dpkgArch" in \
        amd64|arm64) \
# arches officialy built by upstream
            echo "deb [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list \
            && apt-get update \
            ;; \
        *) \
# we're on an architecture upstream doesn't officially build for
# let's build binaries from the published source packages
            echo "deb-src [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list \
            \
# new directory for storing sources and .deb files
            && tempDir="$(mktemp -d)" \
            && chmod 777 "$tempDir" \
# (777 to ensure APT's "_apt" user can access it too)
            \
# save list of currently-installed packages so build dependencies can be cleanly removed later
            && savedAptMark="$(apt-mark showmanual)" \
            \
# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source --compile $nginxPackages \
            ) \
# we don't remove APT lists here because they get re-downloaded and removed later
            \
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)
            && apt-mark showmanual | xargs apt-mark auto > /dev/null \
            && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \
            \
# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)
            && ls -lAFh "$tempDir" \
            && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \
            && grep '^Package: ' "$tempDir/Packages" \
            && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \
# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes")
#   Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
#   ...
#   E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages  Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
            && apt-get -o Acquire::GzipIndexes=false update \
            ;; \
    esac \
    \
    && apt-get install --no-install-recommends --no-install-suggests -y \
                        $nginxPackages \
                        gettext-base \
                        curl \
    && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
    \
# if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
    && if [ -n "$tempDir" ]; then \
        apt-get purge -y --auto-remove \
        && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
    fi \
# forward request and error logs to docker log collector
    && ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log \
# create a docker-entrypoint.d directory
    && mkdir /docker-entrypoint.d

COPY docker-entrypoint.sh /
COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d
COPY 20-envsubst-on-templates.sh /docker-entrypoint.d
COPY 30-tune-worker-processes.sh /docker-entrypoint.d
ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 80

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]

 

问题2: 分析 case 指令分支

我们继续往下走,来到分离后的 RUN 这里:

RUN NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg; \
    dpkgArch="$(dpkg --print-architecture)" \
    && nginxPackages=" \
        nginx=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \
    " \
    && case "$dpkgArch" in \
        amd64|arm64) \
# arches officialy built by upstream
            echo "deb [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list \
            && apt-get update \
            ;; \
        *) \

上面的脚本中,官方的注释已经点明了这段脚本意图:
从上游获取官方构建的产物

arches officialy built by upstream

 
留意到这里用到了 case 指令来检查当前的芯片架构,满足条件时就会直接下载官方发布的打包好的 nginx

dpkgArch="$(dpkg --print-architecture)"
case "$dpkgArch" in amd64|arm64)

一般系统都会进入这个分支,但是我们的目的是为了重新打包 nginx。所幸继续往下观察,发现了
默认的情况就是手动下载包后在重新构建:

...
# we're on an architecture upstream doesn't officially build for
# let's build binaries from the published source packages
            echo "deb-src [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list \
            \
# new directory for storing sources and .deb files
            && tempDir="$(mktemp -d)" \
            && chmod 777 "$tempDir" \
# (777 to ensure APT's "_apt" user can access it too)
            \
# save list of currently-installed packages so build dependencies can be cleanly removed later
            && savedAptMark="$(apt-mark showmanual)" \
            \
# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source --compile $nginxPackages \
            ) \
...

那么我们简单的删除 case 指令,只留下默认情况的代码就可以强制从源码构建 nginx 了。
 

问题3: 从源码切入,加入第三方包编译

根据注释引导,我们了解到下面这段代码就是从源码构建的主要流程

# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source --compile $nginxPackages \
            ) \

这里的核心指令是 apt-get source --compile $nginxPackages, 主要意图是下载 $nginxPackages
指定的多个包的源码并进行编译。那么我要做的就是要拆开这条指令,将它拆成 下载编译 两个流程,
这样我就可以通过拷贝指令,将第三方包的源码放置在下载后的源码目录中,再让他们一起编译。

好,目标明确,查阅文档:

首先我 Google 了 apt-get 的文档,这里遇到一个迷惑问题,Google 的搜索结果里面,排在前面的是:

https://linux.die.net/man/8/apt-get

在这里插入图片描述

 
这文档一眼看上去好像没什么问题,但是拉到 source --compile 说明时,发现这个文档介绍的 --compile
参数的效果等同于用 rpmbuild 来编译源码包。但是我的目标环境是 Ubuntu, 用的是 dpkg
在这里插入图片描述
 

由此,我还去看了一眼互联网档案馆,发现这个网站 07 年上线时介绍的是 dpkg 版本,为什么现在变成了只剩
rpmbuild 了?
https://web.archive.org/web/20070711153000/https://linux.die.net/man/8/apt-get

在这里插入图片描述
 

我寻思着 dpkg 也没有被淘汰呀,真是百思不得其解。这里就不管了,我重新找了 dpkg 版本
的文档来看。

http://ccrma.stanford.edu/planetccrma/man/man8/apt-get.8.html

从文档的介绍可以知道,当携带了 --compile 参数时,apt-get source 会在当前目录完成代码包下载、解压
和编译的操作。也就是我们去掉这个参数就可以不自动进入编译的操作。

It will then find and download into the current directory the newest available version of that source package

 
我们在脚本里面去掉这个参数先:

...
&& apt-get update \
&& apt-get build-dep -y $nginxPackages \
&& ( \
    cd "$tempDir" \
    && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
        apt-get source $nginxPackages \
) \
...

 

dpkg-buildpackage 源码解析

从前面的工作我们得知,在完成代码包的下载和解压后,apt-get 接着就用了 dpkg-buildpackage 这个
工具来完成编译 (或者 rpmbuild)。那么这里有个很严重的问题,文档没有给出它编译时用的参数呀!
不知道参数就调用编译指令可是会有大问题的。

我接下来找了 debian 介绍 dpkg-buildpackage 的文档,但也不能解决实际的问题。

https://www.debian.org/doc/manuals/maint-guide/build.en.html

到这里没办法了,我采用了最原始的方式,查看 apt 的源代码。
万般工具,还得看 C。

这里是 apt 的源码地址:
https://github.com/Debian/apt

下载源代码后,用 vscode 打开,直接搜索 dpkg-buildpackage,就能直接定位出 source 的解析函数

在这里插入图片描述

 
可以从代码中看到,调用 dpkg-buildpackage 时,传入的参数由 buildopts 输出

strprintf(S, "cd %s && %s %s",
	       Dir.c_str(),
	       _config->Find("Dir::Bin::dpkg-buildpackage","dpkg-buildpackage").c_str(),
	       buildopts.c_str());

 

而前面的代码中也给出了 buildopts 的构建流程

std::string buildopts = _config->Find("APT::Get::Host-Architecture");
	 if (buildopts.empty() == false)
	    buildopts = "-a" + buildopts + " ";

	 // get all active build profiles
	 std::string const profiles = APT::Configuration::getBuildProfilesString();
	 if (profiles.empty() == false)
	    buildopts.append(" -P").append(profiles).append(" ");

	 buildopts.append(_config->Find("DPkg::Build-Options","-b -uc"));

 

那答案已经显而易见了,传给 dpkg-buildpackage 的参数默认是 -b -uc -a

接着查阅 dpkg-buildpackage 的文档:
https://manpages.debian.org/testing/dpkg-dev/dpkg-buildpackage.1.en.html

找到关于相关选项的说明:

-a, --host-arch architecture
    Specify the Debian architecture we build for (long option since dpkg 1.17.17). 
    The architecture of the machine we build on is determined automatically, and is also the default for the host machine.

-b: 
    Equivalent to --build=binary or --build=any,all.

-uc, --unsigned-changes
    Do not sign the .buildinfo and .changes files (long option since dpkg 1.18.8).

 

然后同样是 source 源代码中,我们继续往上看,会发现代码是在一个 for 循环中不断进入每个包的
代码目录中,然后再调用 dpkg-buildpackage

   for (auto const &D: Dsc)
   {
      if (unlikely(D.Dsc.empty() == true))
	 continue;
      std::string const Dir = D.Package + '-' + Cache.GetPkgCache()->VS->UpstreamVersion(D.Version.c_str());

      // See if the package is already unpacked
      struct stat Stat;
      if (fixBroken == false && stat(Dir.c_str(),&Stat) == 0 &&
	    S_ISDIR(Stat.st_mode) != 0)
      {
	 ioprintf(c0out ,_("Skipping unpack of already unpacked source in %s\n"),
	       Dir.c_str());
      }
      else
    ...

这样,我们就拆解完了 apt-get source --compile 的步骤了。
 

准备第三方包代码,修改核心包编译规则

apt-get source nginx=1.21.6-1~bullseye, 执行完成后当前目录中除了有上面的三个文件,apt-get 还会帮你自动解压出一个 nginx-1.21.6 目录
在这里插入图片描述

通过观察,nginx-1.21.6/debian 目录就是 nginx_1.21.6-1~bullseye.debian.tar.xz 包里面的 debian 目录
在这里插入图片描述

通过观察,nginx-1.21.6/debian 目录就是 nginx_1.21.6-1~bullseye.debian.tar.xz 包里面的 debian 目录
在这里插入图片描述
 

小结一
apt-get source nginx=1.21.6-1~bullseye 会下出三个文件,其中有一个原始源码包和特定平台依赖包,
nginx_1.21.6.orig.tar.gznginx_1.21.6-1~bullseye.debian.tar.xz, 附加一个
包的校验信息描述文件。然后 apt-get 会将两个源码包的内容解压到当前目录的 nginx-1.21.6 文件夹中

 
编译过程清查

同样是在 dpkg-buildpackage 的文档中,提到了 build 钩子会和 debian/rules 协同进行编译。

在这里插入图片描述

 
那么我们进一步查看 nginx-1.21.6/debian/rules 文件,可以找到有配置 configure 的详细指令

config.env.%:
        dh_testdir
        mkdir -p $(BUILDDIR_$*)
        cp -Pa $(CURDIR)/auto $(BUILDDIR_$*)/
        cp -Pa $(CURDIR)/conf $(BUILDDIR_$*)/
        cp -Pa $(CURDIR)/configure $(BUILDDIR_$*)/
        cp -Pa $(CURDIR)/contrib $(BUILDDIR_$*)/
        cp -Pa $(CURDIR)/man $(BUILDDIR_$*)/
        cp -Pa $(CURDIR)/src $(BUILDDIR_$*)/
        touch $@

config.status.nginx: config.env.nginx
        cd $(BUILDDIR_nginx) && \
        CFLAGS="" ./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt="$(CFLAGS)" --with-ld-opt="$(LDFLAGS)"
        touch $@

config.status.nginx_debug: config.env.nginx_debug
        cd $(BUILDDIR_nginx_debug) && \
        CFLAGS="" ./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt="$(CFLAGS)" --with-ld-opt="$(LDFLAGS)" --with-debug
        touch $@

好我们先暂停在这里,去了解一下 nginx 添加自定义模块的方法
 

nginx 在编译时加入动态模块

nginx 关于模块编译的说明 https://nginx.org/en/docs/njs/install.html#install_package

$ ./configure --add-dynamic-module=path-to-njs/nginx

官方说明如果要在编译时加入动态模块一起编译,在 configure 编译指令中加入 --add-dynamic-module=/path/to/my-module 即可。
我们要添加的模块 ngx_http_subs_filter_module 是代码引入,所以需要动态编译。

 
小结二

通过梳理编译流程,已经确定了是要修改 nginx-1.21.6/debian/rules 文件,在其中的 configure 指令中用 --add-dynamic-module=/path/to/my-module 的方式来加入我们需要添加的模块。

修改结果大致如下:

...CFLAGS="" ./configure --prefix=/etc/nginx --add-dynamic-module=/mymodule/ngx_http_subs_filter_module 

具体的操作步骤可以描述为:

  1. 执行 apt-get source $nginxPackages 让 apt-get 下载指定版本的源码包并帮我们解压好
  2. 修改 nginx-1.21.6/debian/rules 文件中的 configure 编译指令,使用 --add-dynamic-module=/path/to/my-module 加入需要的模块
  3. 再回到下载源码的目录执行 cd nginx-1.21.6 && dpkg-buildpackage -b -uc -a $dpkgArch, 同时也需要对每个下载的源码包执行, 这样的流程和 Dockerfile 里面的 apt-get source --compile $nginxPackages 差不多

以下是根据 Dockerfile 步骤在临时目录中执行 apt-get source --compile $nginxPackages 后的目录结构

在这里插入图片描述
 

准备第三方模块代码

在下面的网址下载 ngx-http-substitutions-filter-module 模块的源码
https://github.com/yaoweibin/ngx_http_substitutions_filter_module

然后将代码文件夹解压到当前的工程目录

在这里插入图片描述

拷贝一份 nginx-1.21.6/debian/rules 文件,做以下修改

在这里插入图片描述

 
然后在 Dockerfile 开头加入两条 COPY 指令将第三方模块代码和需要替换的 debian/rules 文件
拷贝到镜像中。

FROM debian:bullseye-slim

LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

ENV NGINX_VERSION   1.23.1
ENV NJS_VERSION     0.7.6
ENV PKG_RELEASE     1~bullseye

COPY ./ngx-http-substitutions-filter-module-src-master /mymodule/ngx_http_subs_filter_module
COPY ./debian-rules /mymodule/debian-rules
...

 
开始改造

到这里就万事具备了,我们直接将原 Dockerfile 内下载编译模块包的部分修改成下面的内容:

改造前:

...
# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source --compile $nginxPackages \
            ) \
...

改造后:

# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source $nginxPackages \
                && cp /mymodule/debian-rules "./nginx-$NGINX_VERSION/debian/rules" \
                && for dir in nginx*/; do \
                        cd "$dir"; \
                        dpkg-buildpackage -b -uc -a "$dpkgArch"; \
                        cd ..; \
                done; \
            ) \

至此,我们就完成了第三方模块的编译工作了。

 

问题4:权限不足问题

如果遇到这个问题可以修改,否则跳过。

Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)

在下面的问答中找到了一个解决方式

https://askubuntu.com/questions/1160926/local-deb-file-repository-failes-during-apt-get-update

将 89 行的

apt-get -o Acquire::GzipIndexes=false update

改成

apt-get -o Acquire::GzipIndexes=false -o APT::Sandbox::User=root update

 

问题5:模块文件缺失

在完成上面的编译工作后,我尝试打包了一下镜像,此时虽然没有报错,但是我隐约感觉肯定还有点问题。然后在
上面我们找到的 debian-rules 中,看到了重要的两个配置:

--modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf 

这里配置了镜像内部 nginx 模块的存放路径和 nginx 的配置路径。

那么我们用 dive 工具查看镜像内部文件。BTW,dive 工具的使用可以看我另一篇博文:

https://blog.csdn.net/qq_34727886/article/details/136448207

我们去到 /usr/lib/nginx/modules 一看,这里怎么没有我们编译完成的第三方模块呢?我个人认为应该是
非官方模块不自动跟踪依赖了。而且到这里已经花了很多时间,我选择了最简单的拷贝方案解决这个问题。

在这里插入图片描述

 
这里将原本脚本中删除临时文件的指令注释掉,然后重新构建镜像。

...
    && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
    # if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
    && if [ -n "$tempDir" ]; then \
        apt-get purge -y --auto-remove \
        && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
    fi \
...

 
使用 dive 工具看到 $tempDir/nginx-$NGINX_VERSION 下面有个软连接 objs 链接到了
$tempDir/nginx-$NGINX_VERSION/debian/build-nginx/objs

在这里插入图片描述
 

而在这个目录下面,就有我们导入的第三方模块的编译产物 ngx_http_subs_filter_module.so

在这里插入图片描述

 
接着可以在容器内找到编译的第三方模块存在于 "$tempDir/nginx-$NGINX_VERSION/objs/ngx_http_subs_filter_module.so",
那么我们简单的在后面加上一条 cp 命令,将第三方模块放到 /usr/lib/nginx/modules 就行,不
cp 过去后面这个临时目录就会整个删掉。

同时我们也可以在这里加上一条清除指令 rm -rf /mymodule, 清理我们放进镜像的第三方模块编译辅助文件。

...
    && cp "$tempDir/nginx-$NGINX_VERSION/objs/ngx_http_subs_filter_module.so" /usr/lib/nginx/modules \
    && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
    && rm -rf /mymodule \
...

到这里,我们接下来就执行打包命令,然后等待结束就行。

打包命令回顾:

docker build \
 --build-arg http_proxy=http://xxx:7890 \
 --build-arg https_proxy=http://xxx:7890 \
 -t test-nginx:1.0 . --no-cache 2>&1 | tee build.log

等看到了下面的打包日志,就是打包正常结束了。

#13 exporting to image
#13 exporting layers
#13 exporting layers 0.2s done
#13 writing image sha256:067641f4087688634d9b741854f1c848019563c5765defbf8e75f813ac3bebd6 done
#13 naming to docker.io/library/test-nginx:test done
#13 DONE 0.2s

可以再次使用 dive 查看最终产物中有没有我们要的第三方模块的 so

在这里插入图片描述

 

问题6:模块配置文件

记得我们前面看到过 nginx 的编译配置 --conf-path=/etc/nginx/nginx.conf,我们最终要用
docker-compose.yml 将这个配置映射到本地目录,然后在里面要加上下面一句话来动态加载我们编译
的第三方动态模块。

load_module modules/ngx_http_subs_filter_module.so;

这样,我们的第三方模块就能正常使用了。

问题7: docker-entrypoint.sh 没有权限

chmod + x docker-entrypoint.sh 后再构建镜像就行
 

尾声

走完上面所有流程,验证了镜像没有问题后,就可以把我们前面分开的两条 RUN 指令合成一条了,然后对比
我们打出来的镜像和官方镜像的大小,仅多了 1M, 完美!

在这里插入图片描述

最终我们修改完的 Dockerfile 如下:

#
# NOTE: THIS DOCKERFILE IS GENERATED VIA "update.sh"
#
# PLEASE DO NOT EDIT IT DIRECTLY.
#
FROM debian:bullseye-slim

LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

ENV NGINX_VERSION   1.21.6
ENV NJS_VERSION     0.7.3
ENV PKG_RELEASE     1~bullseye

COPY ./ngx-http-substitutions-filter-module-src-master /mymodule/ngx_http_subs_filter_module
COPY ./debian-rules /mymodule/debian-rules

RUN set -x \
# create nginx user/group first, to be consistent throughout docker variants
    && addgroup --system --gid 101 nginx \
    && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \
    && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
    NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg; \
    export GNUPGHOME="$(mktemp -d)"; \
    found=''; \
    for server in \
        hkp://keyserver.ubuntu.com:80 \
        pgp.mit.edu \
    ; do \
        echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
        gpg1 --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
    done; \
    test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
    gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" ; \
    rm -rf "$GNUPGHOME"; \
    apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \
    && dpkgArch="$(dpkg --print-architecture)" \
    && nginxPackages=" \
        nginx=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \
        nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \
    " \
    # let's build binaries from the published source packages
    && echo "deb [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list \
    && echo "deb-src [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list \
            \
# new directory for storing sources and .deb files
            && tempDir="$(mktemp -d)" \
            && chmod 777 "$tempDir" \
# (777 to ensure APT's "_apt" user can access it too)
            \
# save list of currently-installed packages so build dependencies can be cleanly removed later
            && savedAptMark="$(apt-mark showmanual)" \
            \
# build .deb files from upstream's source packages (which are verified by apt-get)
            && apt-get update \
            && apt-get build-dep -y $nginxPackages \
            && ( \
                cd "$tempDir" \
                && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \
                    apt-get source $nginxPackages \
                && cp /mymodule/debian-rules "./nginx-$NGINX_VERSION/debian/rules" \
                && for dir in nginx*/; do \
                        cd "$dir"; \
                        dpkg-buildpackage -b -uc -a "$dpkgArch"; \
                        cd ..; \
                done; \
            ) \
# we don't remove APT lists here because they get re-downloaded and removed later
            \
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
# (which is done after we install the built packages so we don't have to redownload any overlapping dependencies)
            && apt-mark showmanual | xargs apt-mark auto > /dev/null \
            && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \
            \
# create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be)
            && ls -lAFh "$tempDir" \
            && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \
            && grep '^Package: ' "$tempDir/Packages" \
            && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \
# work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes")
#   Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
#   ...
#   E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages  Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied)
            && apt-get -o Acquire::GzipIndexes=false update \
    && apt-get install --no-install-recommends --no-install-suggests -y \
                        $nginxPackages \
                        gettext-base \
                        curl \
    && cp "$tempDir/nginx-$NGINX_VERSION/objs/ngx_http_subs_filter_module.so" /usr/lib/nginx/modules \
    && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \
    && rm -rf /mymodule \
    \
# if we have leftovers from building, let's purge them (including extra, unnecessary build deps)
    && if [ -n "$tempDir" ]; then \
        apt-get purge -y --auto-remove \
        && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \
    fi \
# forward request and error logs to docker log collector
    && ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log \
# create a docker-entrypoint.d directory
    && mkdir /docker-entrypoint.d

COPY docker-entrypoint.sh /
COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d
COPY 20-envsubst-on-templates.sh /docker-entrypoint.d
COPY 30-tune-worker-processes.sh /docker-entrypoint.d
ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 80

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]

原版 Docckerfile: https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/mainline/debian/Dockerfile
 

结语

呼,又是一篇长文创作,真是历经八十一难才搞定这个问题。作为刚接触 Docker 没几天的新人,就要来解决
这个大坑,心态是崩得要死。这次的问题查阅的文档数也是目前最多的,都到底层代码了。这个问题其实我很早就
做完,但是陆陆续续写了很久才把博客梳理出来。接下来要做点其他事情了,这篇博客真的很费时。

不过这一路闯下来,也算是酣畅淋漓。

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在 macOS 上安装 nginx 的第三方模块可以使用 Homebrew 包管理器来简化过程。以下是安装第三方模块的步骤: 1. 首先,确保您已经安装了 Homebrew。如果您还没有安装,可以在终端中运行以下命令来安装 Homebrew: ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` 2. 安装 nginx。在终端中运行以下命令来使用 Homebrew 安装 nginx: ``` brew install nginx ``` 3. 找到您想要安装的第三方 nginx 模块。您可以通过在搜索引擎上搜索或访问模块官方网站来找到合适的模块。 4. 下载并解压第三方模块的源代码。将源代码解压到一个您可以方便访问的位置。 5. 进入解压后的模块源代码目录,并使用 `./configure` 命令配置编译选项。在这个命令中,您可以通过添加 `--add-dynamic-module=/path/to/module` 来指定要安装的模块。例如: ``` ./configure --add-dynamic-module=/path/to/module ``` 请将 `/path/to/module` 替换为您要安装的模块的实际路径。 6. 完配置后,运行 `make` 命令编译 nginx。 7. 编译完后,在终端中运行以下命令将编译好的模块复制到 nginx模块目录: ``` cp objs/*.so /usr/local/Cellar/nginx/{version}/libexec/modules/ ``` 请将 `{version}` 替换为您当前安装的 nginx 本号。 8. 在终端中运行以下命令启动 nginx: ``` brew services start nginx ``` 现在,您已经功安装了第三方模块,并且可以在 nginx 的配置文件中启用和配置它们。 请注意,安装第三方模块可能需要一些编译工具和依赖项。如果出现任何错误或依赖项缺失,您可能需要安装相应的工具和库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值