云计算百科
云计算领域专业知识百科平台

apt-get 补的库,一次重建全没了

我把一台 PHP 容器的 php -m 调到零告警,四个扩展全都能加载。后来在面板里改了一次环境设置,四个扩展全挂回去了。

补的东西没进镜像。它一直在容器的可写层里。

这台是 1Panel v2 起的环境,PHP 8.4.25,镜像 1panel-php-fpm:8.4.25,基础镜像 Debian 13(trixie),服务器 4 核 8G。事情出在 9 月 16 号和 17 号这两天,下面的回显数值都原样保留。

容器名我在下面统一用变量,你在 1Panel 里起的名字是什么就填什么。

# 换成你在 1Panel 里给 PHP 运行环境起的名字(= 容器名)
export PHP_CTN="php84"

同样是往容器里动手,结果完全不一样。

docker restart "$PHP_CTN" 只重启进程,容器文件系统原封不动,你 apt-get 补的库和命令都还在,不用管它。

容器重建就全丢了。docker compose up、在 1Panel 里改环境设置、升级镜像,这三种都算重建。

#mermaid-svg-1XCKDoSSDK4ZpZoo{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1XCKDoSSDK4ZpZoo .error-icon{fill:#552222;}#mermaid-svg-1XCKDoSSDK4ZpZoo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1XCKDoSSDK4ZpZoo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .marker.cross{stroke:#333333;}#mermaid-svg-1XCKDoSSDK4ZpZoo svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1XCKDoSSDK4ZpZoo p{margin:0;}#mermaid-svg-1XCKDoSSDK4ZpZoo .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .cluster-label text{fill:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .cluster-label span{color:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .cluster-label span p{background-color:transparent;}#mermaid-svg-1XCKDoSSDK4ZpZoo .label text,#mermaid-svg-1XCKDoSSDK4ZpZoo span{fill:#333;color:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .node rect,#mermaid-svg-1XCKDoSSDK4ZpZoo .node circle,#mermaid-svg-1XCKDoSSDK4ZpZoo .node ellipse,#mermaid-svg-1XCKDoSSDK4ZpZoo .node polygon,#mermaid-svg-1XCKDoSSDK4ZpZoo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .rough-node .label text,#mermaid-svg-1XCKDoSSDK4ZpZoo .node .label text,#mermaid-svg-1XCKDoSSDK4ZpZoo .image-shape .label,#mermaid-svg-1XCKDoSSDK4ZpZoo .icon-shape .label{text-anchor:middle;}#mermaid-svg-1XCKDoSSDK4ZpZoo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .rough-node .label,#mermaid-svg-1XCKDoSSDK4ZpZoo .node .label,#mermaid-svg-1XCKDoSSDK4ZpZoo .image-shape .label,#mermaid-svg-1XCKDoSSDK4ZpZoo .icon-shape .label{text-align:center;}#mermaid-svg-1XCKDoSSDK4ZpZoo .node.clickable{cursor:pointer;}#mermaid-svg-1XCKDoSSDK4ZpZoo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .arrowheadPath{fill:#333333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1XCKDoSSDK4ZpZoo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1XCKDoSSDK4ZpZoo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1XCKDoSSDK4ZpZoo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1XCKDoSSDK4ZpZoo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .cluster text{fill:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo .cluster span{color:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1XCKDoSSDK4ZpZoo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1XCKDoSSDK4ZpZoo rect.text{fill:none;stroke-width:0;}#mermaid-svg-1XCKDoSSDK4ZpZoo .icon-shape,#mermaid-svg-1XCKDoSSDK4ZpZoo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1XCKDoSSDK4ZpZoo .icon-shape p,#mermaid-svg-1XCKDoSSDK4ZpZoo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1XCKDoSSDK4ZpZoo .icon-shape .label rect,#mermaid-svg-1XCKDoSSDK4ZpZoo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1XCKDoSSDK4ZpZoo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1XCKDoSSDK4ZpZoo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1XCKDoSSDK4ZpZoo :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

docker restart

容器重建

镜像层只读,来自 1panel-php-fpm:8.4.25

容器可写层你 apt-get 补的库都写在这里

执行了什么操作?

容器文件系统保留补装项都还在

可写层被丢弃补装项全部消失

这条我当时判断错过一次。我以为 unzip 不依赖任何库,重建之后应该还在。错了。unzip 同样是 apt-get 装的,跟那些库一样活在容器的可写层里。决定去留的是你执行了哪种操作,跟这个包有没有依赖链没关系。

两层文件系统

Docker 容器 = 镜像层(只读)+ 容器可写层。

镜像层的内容来自 docker pull 拉下来的镜像,所有容器共享,容器内的任何操作都改不动它。

你在容器里做的写操作,apt-get install、改文件、建文件,全都落在可写层。这一层是这个容器实例私有的。

docker restart 只是把容器进程重启一遍,可写层不动,补装项就还在。

重建的本质是销毁旧容器实例、用同一个镜像重新建一个新实例。新实例的可写层是全新的空白层,旧的连带里面所有补装内容一起被丢弃。

你补的东西从头到尾就没进过镜像,只在某一代容器的可写层里待着。

第一次补库时,我是照着报错信息一个个试包名的。重放的时候换了个思路:不记当时敲了哪条命令,记怎么重新推出这条命令。

包名会变,后面有真实案例。存命令迟早过期,存方法不会。

第一步是问 PHP 自己扩展目录在哪。硬编码路径是个坑,目录名里带着 PHP 的 API 版本号:

/usr/local/lib/php/extensions/no-debug-non-zts-20240924/
^^^^^^^^^^ 换 PHP 版本就变

换个小版本,20240924 这段就变了。

# 本机验证:这条能直接拿到扩展目录
docker exec -i "$PHP_CTN" php -r 'echo ini_get("extension_dir"), "\\n";'

然后把遍历扩展目录、逐个 ldd、只留 not found 打包成一条:

# 遍历所有扩展 .so,找出依赖缺失的那些
docker exec -i "$PHP_CTN" bash -c '
EXT_DIR="$(php -r "echo ini_get(\\"extension_dir\\");")"
for so in "$EXT_DIR"/*.so; do
[ -e "$so" ] || continue
miss=$(ldd "$so" 2>/dev/null | grep "not found")
if [ -n "$miss" ]; then
echo "— $(basename "$so") —"
echo "$miss"
fi
done'

三个细节,都是踩过才知道的。

[ -e "$so" ] || continue:通配符一个都没匹配到的时候,$so 会是字面量 *.so,这句把它挡掉。

2>/dev/null:有些 .so 不是动态库,ldd 会往 stderr 刷 not a dynamic executable。

ldd 只回答缺不缺,不回答该不该装。缺库的扩展不代表你都需要,补之前先确认哪些扩展是项目真在用的。

这套遍历加过滤的脚本逻辑,我在同类环境上用替身函数做过 dry-run,把 ldd 换成模拟输出,遍历、continue 保护、basename、计数全部符合预期。稿子里标「本机验证」的就是这一类,它验的是脚本逻辑,不是我那台机器上的结果。

补库之前,这条命令当时还是硬编码路径的版本,跑出来的是:

— gd.so —
libpng16.so.16 => not found
libavif.so.16 => not found
libwebp.so.7 => not found
libjpeg.so.62 => not found
libXpm.so.4 => not found
libfreetype.so.6 => not found
— intl.so —
libicuio.so.76 => not found
libicui18n.so.76 => not found
libicuuc.so.76 => not found
— zip.so —
libzip.so.5 => not found
— memcached.so —
libmemcached.so.11 => not found

四个扩展,一共缺 11 个共享库,9 月 16 号实测。

补完再跑这条复验:

# 复验:有告警就有输出,空输出 = 全部加载成功
docker exec -i "$PHP_CTN" php -m 2>&1 | grep -i 'Unable to load'

9 月 17 号实测,空输出,零告警。

重建之后怎么重放

按这个顺序走一遍:

# ① 先看丢没丢 —— 有输出说明扩展又挂了
docker exec -i "$PHP_CTN" php -m 2>&1 | grep -i 'Unable to load'

# ② 反查现在缺哪些库

# ③ 补库
docker exec -i "$PHP_CTN" bash -lc '
apt-get update -qq
apt-get install -y –no-install-recommends unzip
# 包名逐个试:Debian 版本过渡期同名包会有多个候选
for p in libzip5 libzip4; do
apt-get install -y –no-install-recommends "$p" 2>/dev/null && { echo "libzip via $p OK"; break; }
done
apt-get install -y –no-install-recommends libjpeg62-turbo libwebp7 libfreetype6 libxpm4 libavif16
for p in libpng16-16t64 libpng16-16; do
apt-get install -y –no-install-recommends "$p" 2>/dev/null && { echo "libpng via $p OK"; break; }
done
apt-get install -y –no-install-recommends libicu76
# memcached 的库;同样是 t64 过渡改名,包名别按规律硬推
for p in libmemcached11t64 libmemcached11; do
apt-get install -y –no-install-recommends "$p" 2>/dev/null && { echo "libmemcached via $p OK"; break; }
done
'

docker restart "$PHP_CTN"

# ④ 复验
docker exec -i "$PHP_CTN" php -m 2>&1 | grep -i 'Unable to load'

有一件事得说清楚。我在容器里补库的时候没把实际执行的命令存档,事后想重放才发现包名记不全了。上面这段是我事后重建的自适配版本,用 for 循环逐个试候选包名,容忍包名不确定。它不等于当初那条。

更打脸的是,这个自适配版本第一次写出来时,自己就漏了一个扩展:四个扩展里 memcached 那条没写进去,后来拿实际回执逐项核对才发现。

漏一项的后果跟全没装一样,那个扩展照样 Unable to load。只是告警从 4 条变成 1 条,很容易被当成差不多好了。

把它验收干净靠的是遍历所有扩展逐个 ldd,人眼会漏。所以这一课比「记得存档」更值钱:存下来的清单会写错,能自动验收的方法不会。

顺便说一句,unzip 这个包名是确定的,实测装的是 unzip 6.0-29+deb13u1,没有依赖链,是清单里最稳的一条。所以 zip 扩展躺着的时候,unzip 命令往往是你唯一剩下的解包通道。

Debian 正在做 64 位 time_t 过渡(t64),一批包改了名。

libpng16.so.16 这个库,包名从 libpng16-16 变成了 libpng16-16t64。你的补库命令要是写死了旧名,在新系统上就是 E: Unable to locate package。

同一个坑我踩了两次。libmemcached.so.11 对应的包,我按 Debian 的命名规律(lib<name><so 主版本>)推成了 libmemcached11,到 Debian 13 上一看,实际叫 libmemcached11t64,又是 t64 改名。按规律推断包名一样会错。

所以上面那段用了 for p in libpng16-16t64 libpng16-16 逐试的写法。同样的坑还有 ICU 库,libicu76 里的 76 是 ICU 版本号,跟着系统走。

重放清单得有人存、有人找、有人执行。它天然适合放进项目仓库,比如 scripts/php-ext-replay.sh,前提是你真的写了、下次真的想起来。

重建之后有一段空窗期

从重建完成,到你想起去重放,中间这段时间服务是带病运行的。扩展没加载,相关功能直接报错。重建要是发生在你改完别的配置顺手做了个 compose up 的时候,这个窗口可能一直持续到你下次发现问题。

上面三条坑的共同原因只有一个:补装没进镜像。那就让它进镜像,基于 1Panel 的 PHP 镜像构建自己的镜像:

# Dockerfile —— 基于 1Panel 的 PHP 运行环境镜像,把补装固化进镜像层
FROM 1panel-php-fpm:8.4.25

# Debian 13(trixie)源已就绪,一次性补共享库 + unzip
# 注意:libpng / libmemcached 在 t64 过渡期都有两个候选包名,各用一个循环逐个试
# —— 两个循环不能合并:合并后前者一命中就 break,后者会被整个漏掉
RUN set -eux; \\
apt-get update -qq; \\
apt-get install -y –no-install-recommends \\
unzip \\
libzip5 \\
libjpeg62-turbo libwebp7 libfreetype6 libxpm4 libavif16 \\
libicu76; \\
for p in libpng16-16t64 libpng16-16; do \\
apt-get install -y –no-install-recommends "$p" && break; \\
done; \\
for p in libmemcached11t64 libmemcached11; do \\
apt-get install -y –no-install-recommends "$p" && break; \\
done; \\
apt-get clean; \\
rm -rf /var/lib/apt/lists/*

写这个 Dockerfile 时踩到一个坑:注释不能写在续行 RUN 的中间。Docker 解析 \\ 续行时会把注释行按普通内容并进来,整条指令被从注释处截断,后半段的 for 循环和 apt-get clean 全丢了。所以上面把所有注释都挪到了 RUN 之前。

这类语法错误在构建时不一定报错,只是少装了几个包。建议用 hadolint 之类的工具过一遍。

构建、打标、在 1Panel 里把这个镜像指过去。此后再怎么重建,补装项都随镜像一起回来。

我这次为什么没走这条路?两个现实原因。

一是当时的目标是尽快让环境可用。补库当场就解决了阻塞,自建镜像要额外处理构建、打标、以及 1Panel 的运行环境关联。

二是会脱离 1Panel 的自动管理。PHP 运行环境是面板托管的,换成自建镜像后,面板的升级运行环境这类操作行为就不再可预期,升级路径得你自己维护。

怎么取舍:你只是这次把环境搭起来,重放清单够了;你要长期运维、还会反复重建,构建自定义镜像是明显更划算的一步。

赞(0)
未经允许不得转载:网硕互联帮助中心 » apt-get 补的库,一次重建全没了
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!