問題背景
線上生產環境用的 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-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.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_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
具體的操作步驟可以描述為:
- 執行
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-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
沒幾天的新人,就要來解決
這個大坑,心態是崩得要死。這次的問題查閱的文檔數也是目前最多的,都到底層代碼了。這個問題其實我很早就
做完,但是陸陸續續寫了很久才把博客梳理出來。接下來要做點其他事情了,這篇博客真的很費時。
不過這一路闖下來,也算是酣暢淋漓。