在旧版 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-slimLABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"ENV NGINX_VERSION   1.21.6
ENV NJS_VERSION     0.7.6
ENV PKG_RELEASE     1~bullseyeRUN 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 upstreamecho "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 packagesecho "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.dCOPY 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 80STOPSIGNAL SIGQUITCMD ["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 upstreamecho "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 packagesecho "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 profilesstd::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 architectureSpecify 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-changesDo 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 unpackedstruct 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_testdirmkdir -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.nginxcd $(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_debugcd $(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-debugtouch $@

好我们先暂停在这里,去了解一下 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-slimLABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"ENV NGINX_VERSION   1.23.1
ENV NJS_VERSION     0.7.6
ENV PKG_RELEASE     1~bullseyeCOPY ./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-slimLABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"ENV NGINX_VERSION   1.21.6
ENV NJS_VERSION     0.7.3
ENV PKG_RELEASE     1~bullseyeCOPY ./ngx-http-substitutions-filter-module-src-master /mymodule/ngx_http_subs_filter_module
COPY ./debian-rules /mymodule/debian-rulesRUN 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.dCOPY 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 80STOPSIGNAL SIGQUITCMD ["nginx", "-g", "daemon off;"]

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

结语

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/14275.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络安全、信息安全、数据安全的定义与区别

信息安全 信息安全是指信息的保密性、完整性、可用性和真实性的保持。从定义角度来说&#xff0c;信息安全没有严格标准定义&#xff0c;但从信息安全涉及的内容出发&#xff0c;信息安全确保信息存储或传输中的信息&#xff0c;不被他人有意或无意的窃取与破坏。这里的“信息”…

Vue3+ts(day07:pinia)

学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学&#xff0c;可以点心心支持一下哈&#xff08;笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】&#xff0c;记录一下学习笔记&#xff0c;用于自己复盘&#xff0c;有需要学…

ENVI光谱识别指导采矿管理者监测铜矿分布

圣地亚哥SRGIS的GIS专家Chile需要利用影像光谱信号勘察Chuquicamata的铜矿分布。 解决方案 Chuquicamata是世界上最大的斑岩铜矿分布区。SRGIS发现西部地区只有有限的矿物和贫瘠的岩石&#xff0c;但东部有铜矿分布。为了进一步测定矿藏的情况&#xff0c;他们开发出一套程序&a…

NoSQL Redis配置与优化

一、关系数据库与非关系型数据库 1. 关系型数据库&#xff1a; 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型&#xff08;二维表格模型&#xff09;基础上&#xff0c;一般面向于记录。 SQL 语句&#xff08;标准数据查询语言&#xff09;就是一种基于关系型…

redis核心面试题一(架构原理+RDB+AOF)

文章目录 0. redis与mysql区别1. redis是单线程架构还是多线程架构2. redis单线程为什么这么快3. redis过期key删除策略4. redis主从复制架构原理5. redis哨兵模式架构原理6. redis高可用集群架构原理7. redis持久化之RDB8. redis持久化之AOF9. redis持久化之混合持久化 0. red…

穷人如何翻身赚钱?不妨试试这5个冷门生意,干好了,收入相当不错

根据统计数据&#xff0c;我国月收入超过3000元的人口已超过4亿&#xff0c;这意味着仍有约10亿人的月收入低于3000元。正因为如此&#xff0c;网络上许多人都自嘲为“穷人”。 然而&#xff0c;穷人真的无法改变自己的命运吗&#xff1f;并非如此。对于渴望赚钱的穷人来说&am…

传统蓝牙模块BR/EDR与低功耗蓝牙模块有什么区别?

传统蓝牙模块BR/EDR与低功耗蓝牙模块有什么区别&#xff1f;下面跟随美迅物联网MesoonRF从多个维度来了解。   概述&#xff1a;低功耗蓝牙采用了高斯频移键控&#xff08;GFSK&#xff09;。这里我们先抛开蓝牙的协议&#xff0c;单纯从Radio的角度看收发通信&#xff0c;Ra…

【Crypto】Url编码

文章目录 Url编码解题感悟 Url编码 Url编码 搞定 小小flag&#xff0c;拿下&#xff01; 解题感悟 有点饿了…

图数据库助力供应链柔性升级

导读 当今市场环境受短视频等流媒体影响&#xff0c;任何风险事件在社交网络中传播速度极其迅速&#xff0c;留给企业的反应时间按分秒计&#xff0c;传统供应链的年度计划面对剧烈变化的市场环境已失去意义。此外&#xff0c;受近年局势动荡的影响&#xff0c;市场需求和供应…

APISIX-简单使用

APISIX-简单使用 这个工具还是很不错的&#xff0c;可视化的配置很清晰 &#xff0c; 想用NGINX的配置模式也是可以的&#xff0c;就是要去修改配置文件了。 APISIX&#xff0c;一个很不错的可视化工具&#xff0c;用来代替Nginx相当不错&#xff0c;可作为Nginx的平替方案&…

【Python进阶】主流电商平台数据分析||数据采集返回商品详情主题链接主图SKU数据

Python是一种高级编程语言&#xff0c;广泛应用于软件开发、数据分析、人工智能、科学计算等领域。在软件开发方面&#xff0c;Python在网站开发、网络编程、桌面软件开发等方面有着广泛的应用。在数据分析和人工智能领域&#xff0c;Python的各种库如NumPy、Pandas、Matplotli…

守护者:ThingsBoard物联网网关在温室环境监测中的应用

系统设计 智慧农业温室大棚系统由传感器及执行设备、数据传输网关、智慧农业温室大棚管理平台组成。 系统支持实时采集温室大棚内的空气温湿度、土壤温湿度、光照和二氧化碳等环境参数&#xff0c;根据农作物的生长需求自动控制温室中电器设备的启停&#xff0c;从而达到植物生…

【Linux】信号集及信号集操作函数

文章目录 一、信号集是什么&#xff1f;二、信号集操作函数1. sigemptyset2. sigfillset3. sigaddset4. sigdelset5. sigismember6. sigprocmask (仅用于读取或更改block表)7. sigpending (仅用于读取pending表) 一、信号集是什么&#xff1f; 对于每个进程, 都有三个信号集, …

k8s集群部署成功后某个节点突然出现notready状态解决办法

通过&#xff1a; kubectl get nodes 查看master1节点为not ready 通过查看日志&#xff1a; journalctl -f -u kubelet.service 看到这里 查看状态&#xff1a; systemctl status kubelet.service 重启一样会报错 执行&#xff1a; swapoff -a 执行后&#xff0c;重启…

pytorch深度学习-环境搭建

1.Anaconda下载&#xff08;首先安装Anaconda不需要先安装Python了&#xff01;&#xff09; 版本 3.11. Download Anaconda Distribution | Anaconda 1.2 跳过注册直接下载 2.安装 直接next, (Install for 可以选择All Users&#xff0c;我选择的是All Users) 点击默认选项…

Qt Designer 使用笔记

目录 qt designer安装 预览 Ctrl R 使用 Qt Designer 设计的ui文件可以通过以下命令转为.py文件 命令行脚本&#xff1a; pycharm工具栏配置&#xff1b; pyqt5也是可以的&#xff1a; 2.2 测试是否配置成功 设置背景颜色&#xff1a; ui收集 qt designer安装 pip ins…

JavaEE-网络初识

文章目录 一、网络背景1.1 起源1.2 国内网络的发展 二、关键概念2.1 网络2.2 设备2.3 ip地址与端口号 三、协议3.1 协议分层3.2 OSI七层模型3.3 TCP/IP五层模型3.4 数据传输过程的简单叙述 一、网络背景 1.1 起源 在国外大概时上世纪70年代左右&#xff0c;网络就出现了&…

U-Mail邮件系统取得多项适配认证,全面支持国产化信创环境

随着信息技术的发展&#xff0c;信息化建设越来越深入到社会各个领域&#xff0c;成为驱动经济社会发展的重要力量。在此背景下&#xff0c;我国正加快构建国家信息安全保障体系&#xff0c;实现自主可控&#xff0c;形成安全可靠的信息技术体系。这正是我们所说的“信创”&…

ssl证书价格一年多少钱?怎么申请?

随着各大平台下架了一年期免费证书&#xff0c;免费证书的有效期都为90天。更多企业选择付费证书。费用是众多用户关心的话题&#xff0c;一年期SSL证书价格在几十到几千元不等。 一年期SSL证书价格查看https://www.joyssl.com/certificate/select/0-1000.html?nid16 下面是…

如何官方查询论文分区,中科院及JCR

中科院分区 有一个小程序&#xff1a;中科院文献情报中心分区表 点2023升级版&#xff0c;输入期刊名 大类1区 JCR分区 进入官方网站 Journal Citation Reports 输入要查询的期刊名&#xff0c;点开 拼命往下拉 这就是根据影响因子的排名&#xff0c;在computer science&am…