问题背景
线上生产环境用的 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_proxy
和 https_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 获取脚本,这里引出了三个小问题:
- 替换
apt-key
的工具是什么? - 替换工具后的脚本要做什么改动?
- 替换声明是否来自官网更新文档?
这里要唠叨几句:
首先我也是初学 docker
,平日里没有太多精力再去关注 docker
和 nginx
的社区,可能有活跃的网友知道怎么改在某一个博客中提及了。但是我到目前为止没有看到问题和我这个完全一样的,所以对那些解决方案我都是持质疑态度,我个人一般是信奉官方文档和 API 定义多一些。
而且说来惭愧,我在定位到是工具问题之前,已经是花了一整天来搜索各种博客的解决方案。但最终没有一个能完美解决我的问题,一天就这么浪费了。由于这个集成方案的探索在下一周就要部署到客户现场,所以浪费的这一天也让我从这里开始到整个问题探索结束,都不会再去看网上的那些博客。同时我也意识到我这个问题应该网上也不会有很好的解决方案。
基于此,我选择直接去问官方人员,他们最清楚该改哪里。幸运的是,我在 nginx
的 github
上提了 issue 后,官方人员第二天就回复我了,这里必须点一个大大的赞。而且官方人员明确指出了我用的 Dockerfile
太老旧了,他们早就替换了新版的脚本,并给出 commit 给我去参考,真的感谢。
改动的 commit:
https://github.com/nginxinc/docker-nginx/commit/38e2690b304b8dca4848f3e70a1fc95837f61510
在管理员提供的 commit 中, 他们把请求 Key 的工具从 apt-key
换成了 gpg1
, 并对原始的 Dockerfile
进行了一些修改,我们照葫芦画瓢就行。
得益于管理员的帮助,问题一完美解决!
插曲, 暂时对 Dokcerfile 进行分层加速调试
学习过 Dockerfile
的 RUN
命令就知道,每个 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.gz
和 nginx_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
具体的操作步骤可以描述为:
- 执行
apt-get source $nginxPackages
让 apt-get 下载指定版本的源码包并帮我们解压好 - 修改
nginx-1.21.6/debian/rules
文件中的configure
编译指令,使用--add-dynamic-module=/path/to/my-module
加入需要的模块 - 再回到下载源码的目录执行
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
工具的使用可以看我另一篇博文:
我们去到 /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
没几天的新人,就要来解决
这个大坑,心态是崩得要死。这次的问题查阅的文档数也是目前最多的,都到底层代码了。这个问题其实我很早就
做完,但是陆陆续续写了很久才把博客梳理出来。接下来要做点其他事情了,这篇博客真的很费时。
不过这一路闯下来,也算是酣畅淋漓。