1Panel 的 PHP 扩展面板上那一排绿勾,不能信。
我把一台全新云服务器交给自己从零重建,OpenResty、MySQL、PHP 全走 1Panel 的容器化环境。装完那天我打开 PHP 扩展配置页,该勾的都勾上,保存、重启,界面干干净净,一个红点都没有。然后我进容器敲了一条命令,回报给我四行Unable to load dynamic library。
这不是 1Panel 的 bug,也不是我勾错了。它是容器化 PHP 环境的一个结构性事实:面板告诉你一切正常,只有命令行会告诉你真相。
先把环境交代清楚。脱离环境谈踩坑等于耍流氓,同一个坑换别的镜像、别的 PHP 版本未必成立。我用的是一台 4 核 8G、200G 系统盘的云服务器,1Panel v2。Web 服务器是容器里的 OpenResty,镜像1panel/openresty:1.31.1.1-2-4-noble。数据库 MySQL 8.4.11 容器,PHP 8.4.25 容器,后者基础镜像 Debian 13(trixie)。应用是 ThinkPHP 8.0,app/目录下 104 个类,依赖由 Composer 管,composer.lock已锁版本。
调用链是一条直线:OpenResty 打到127.0.0.1:9000,进 PHP-FPM 容器,再连 MySQL 容器。三个组件各在独立容器里,靠 1Panel 创建的同名 Docker 网络互通。
为了让命令能直接复制,我先在终端里定义几个变量,后续命令全部复用:
# 换成你在 1Panel 里给 PHP 运行环境起的名字(= 容器名)exportPHP_CTN="php84"# 换成你在 1Panel 里给 MySQL 起的名字exportMYSQL_CTN="mysql84"# 确认两个容器都在跑dockerps--format"table {{.Names}}\t{{.Image}}\t{{.Status}}"|grep-E"$PHP_CTN|$MYSQL_CTN"下文所有回显都是我在自己服务器上跑出来的真实输出,实测于 2026-09-16 和 09-17。为了让命令通用,我把回显里的容器名统一替换成了上面的变量名,除此之外一字未改。
1Panel 的 PHP 扩展页勾选后,最终会落到容器的php -m上。所以验证动作只有一条,别信面板,去问容器:
# 一次筛出本项目关心的扩展 + opcache/zip,看有没有加载告警dockerexec-i"$PHP_CTN"php-m|grep-Ei'pdo_mysql|mbstring|openssl|curl|fileinfo|bcmath|opcache|gd|zip'回显是这样的(实测 2026-09-16):
PHP Warning: PHP Startup: Unable to load dynamic library 'gd' (tried: .../gd.so (libpng16.so.16: cannot open shared object file: No such file or directory)) in Unknown on line 0 PHP Warning: PHP Startup: Unable to load dynamic library 'intl' (tried: .../intl.so (libicuio.so.76: cannot open shared object file: No such file or directory)) in Unknown on line 0 PHP Warning: PHP Startup: Unable to load dynamic library 'zip' (tried: .../zip.so (libzip.so.5: cannot open shared object file: No such file or directory)) in Unknown on line 0 PHP Warning: PHP Startup: Unable to load dynamic library 'memcached.so' (tried: .../memcached.so (libmemcached.so.11: cannot open shared object file: No such file or directory)) in Unknown on line 0 bcmath curl fileinfo mbstring openssl pdo_mysql判读很直接。下面那 6 行是 stdout,真加载成功了;上面 4 行 Warning 是 stderr,加载失败。面板上这 4 个扩展统统显示「已启用」,实际一个都没生效——gd、intl、zip、memcached。
再看 Warning 里括号的内容:
'zip' (tried: .../zip.so (libzip.so.5: cannot open shared object file: No such file or directory))翻译过来是三层:zip.so是存在的(否则不会提示 tried),但zip.so自己要用的libzip.so.5不存在。
这就是 1Panel 扩展面板失真的原因:镜像里把扩展的.so编好了,却漏装了这些.so所依赖的运行库。面板只检查扩展列表里有没有勾这一项,管不到勾了之后dlopen能不能成功。面板状态和实际加载结果,是完全脱钩的两件事。
整条加载过程长这样:
面板只校验第 1 步的勾选状态,第 4 步的dlopen结果它根本管不到。
不要猜缺哪个库。ldd会把一个二进制依赖的动态库逐个列出来,缺的标not found:
# 逐个扩展跑 ldd,只挑出「找不到」的库dockerexec-i"$PHP_CTN"bash-c' for e in gd intl zip memcached; do echo "--- $e ---" ldd /usr/local/lib/php/extensions/no-debug-non-zts-*/$e.so 2>/dev/null | grep "not found" done'--- gd --- 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 --- libicuio.so.76 => not found libicui18n.so.76 => not found libicuuc.so.76 => not found --- zip --- libzip.so.5 => not found --- memcached --- libmemcached.so.11 => not found四个扩展一共缺 11 个共享库。gd缺libpng16、libjpeg、libwebp、libfreetype、libXpm、libavif;intl缺libicuuc、libicui18n、libicuio;zip缺libzip.so.5;memcached缺libmemcached.so.11。
顺带一个副产品:libicuuc.so.76的版本号把基础镜像暴露了,ICU 76 对应 Debian 13(trixie)。对新手来说这是个实用技巧,.so的版本后缀能反推系统版本,比去翻镜像文档快。
另外路径里的no-debug-non-zts-20240924是 PHP 的 API 版本号,不同 PHP 版本不一样。所以我上面用了通配符no-debug-non-zts-*。写死在脚本里,换版本就失效。
知道缺什么之后,最容易犯的错是立刻去补库。别急,先分清哪些扩展真的需要,否则你会为了消掉告警,给一台生产服务器塞进一堆用不到的运行库。
判断依据有两个,都不靠经验。
第一个是composer.lock的require段。Composer 安装时会校验平台依赖(ext-*),但它只认require段,不看你的代码实际调没调。所以composer.lock是一份比「我印象里需要什么」可靠得多的扩展清单。
# 在项目根目录,列出 lock 里所有 ext-* 平台依赖grep-n'"ext-'composer.lock但这里有个很多人会漏的分辨点:composer.lock里有两个段,packages(生产)和packages-dev(开发)。生产部署走的是composer install --no-dev,packages-dev整个不会被装,也就不会被校验。
我写了个小脚本把两个段拆开看(已实跑):
<?php// 解析 composer.lock,把 ext-* 平台依赖按「生产/开发」两个段拆开$lock=json_decode(file_get_contents('composer.lock'),true);foreach(['packages'=>'生产段','packages-dev'=>'开发段']as$sec=>$label){foreach($lock[$sec]??[]as$pkg){foreach(['require','require-dev']as$kind){foreach($pkg[$kind]??[]as$dep=>$ver){// 只关心平台依赖(ext-*),PHP 扩展都在这里if(str_starts_with($dep,'ext-')){printf("[%s] %-28s %-11s -> %s\n",$label,$pkg['name'],$kind,$dep);}}}}}在我本机 PHP 8.4.25 CLI 上,用真实composer.lock跑出来:
[生产段] league/flysystem-local require -> ext-fileinfo [生产段] league/mime-type-detection require -> ext-fileinfo [生产段] topthink/framework require -> ext-ctype [生产段] topthink/framework require -> ext-json [生产段] topthink/framework require -> ext-mbstring [生产段] topthink/think-orm require -> ext-json [生产段] topthink/think-orm require -> ext-pdo [开发段] symfony/polyfill-mbstring require -> ext-iconv [开发段] league/flysystem require-dev -> ext-zip结果很干净,两条结论。生产环境真正被校验的只有 5 条:fileinfo、ctype、json、mbstring、pdo。ext-iconv来自symfony/polyfill-mbstring,而它在packages-dev段,--no-dev时根本不会被校验;ext-zip更彻底,它在league/flysystem的require-dev里,对下游使用者无效。
这里也顺手纠正一个常见说法:ext-zip不是项目依赖,但它确实是 Composer 自己的工具层依赖,解压 dist 包要用。所以它该不该补,跟业务代码用不用 zip 是两件事。
第二个依据是代码级实证。lock 只说包要求什么,还得看我的代码用什么。直接搜函数名:
# 高精度计算(bcmath 提供的是 bcadd/bcsub/bcmul/bcdiv/bccomp 等函数)grep-rn"bcsub\|bcadd\|bcmul\|bcdiv\|bccomp"app# 图像处理(gd)与国际化(intl)的典型调用grep-rn"imagecreatefrom\|getimagesize"appgrep-rn"Collator\|NumberFormatter"app实测下来,bcmath在app/v1/logic/index/GetTang.php:28的bcsub()里被引用了 1 处,必留,砍了接口直接 500。gd、intl、memcached都是 0 处引用,可不装——缓存与 session 都走文件驱动。
最终清单是 5 条 lock 生产硬依赖,加bcmath的代码实证,加运行时必需的curl和openssl:
bcmath ctype curl fileinfo json mbstring openssl pdo_mysqliconv属于开发段要求、且是 PHP 默认编译项(镜像自带),核验一下不吃亏,但不必为它专门做什么。
清单定完,动作就剩下取舍。我这次的路线是全救,11 个库全补、4 个扩展全留,理由是第六节那个坑:动扩展勾选可能触发容器重建。
补库命令用的是自适配写法,Debian 13 上有几个包名处于过渡期,所以对同义词做逐个尝试:
dockerexec-i"$PHP_CTN"bash-lc' set -e apt-get update -qq # ① unzip 命令:包名确定、无依赖链 apt-get install -y --no-install-recommends unzip # ② libzip(给 zip 扩展补依赖)——Debian 13 包名二选一 for p in libzip5 libzip4; do apt-get install -y --no-install-recommends "$p" 2>/dev/null && { echo "libzip via $p OK"; break; } done # ③ gd 的图像库 apt-get install -y --no-install-recommends libjpeg62-turbo libwebp7 libfreetype6 libxpm4 libavif16 # libpng 有 t64 过渡改名(libpng16-16 -> libpng16-16t64),同样逐个试 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 # ④ intl 的 ICU 库 apt-get install -y --no-install-recommends libicu76 'dockerrestart"$PHP_CTN"顺序很重要。如果你打算走「只留需要的扩展」这条路,先在 1Panel 改扩展列表,之后再到容器里apt-get。反过来做,改扩展时一旦触发容器重建,你刚补的库会被一起冲掉,白干一次。
复验只有一条命令:
# 有告警就会有输出;空输出 = 全部加载成功dockerexec-i"$PHP_CTN"php-m2>&1|grep-i'Unable to load'回显(实测 2026-09-17):
(无输出)Day 1 这里躺着 4 条 Warning,现在空了。再打一次全量确认:
dockerexec-i"$PHP_CTN"php-m[PHP Modules] bcmath Core ctype curl date dom fileinfo filter ftp gd gettext hash iconv intl json libxml mbstring memcached mysqli mysqlnd openssl pcntl pcre PDO pdo_mysql pdo_sqlite Phar posix random readline redis Reflection session shmop SimpleXML soap sockets SPL sqlite3 standard sysvsem tokenizer xml xmlreader xmlrpc xmlwriter Zend OPcache zip zlib [Zend Modules] Zend OPcachegd、intl、memcached、zip全部出现在列表里,48 个 PHP 模块加 1 个 Zend 模块,零告警。
顺带把解包能力补成双通道。原因在第 2 节那个 Warning 里:zip扩展依赖 6 个容器内apt-get装的库,链条长、易断;而unzip是独立二进制,装上就基本不会挂。两条路互为备份,Composer 解压 dist 包时哪条通走哪条:
# 补装 + 复验dockerexec-i"$PHP_CTN"bash-lc'apt-get update -qq && apt-get install -y --no-install-recommends unzip'dockerexec-i"$PHP_CTN"bash-c'command -v unzip && unzip -v | head -1'/usr/bin/unzip UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.安装过程里会刷出一堆debconf: unable to initialize frontend ... falling back to Noninteractive。这是无害提示——docker exec没有 TTY,debconf 依次尝试 Dialog、Readline、Teletype 前端都失败,最终降级到 Noninteractive,包正常装完。另外26 not upgraded也是常规提示,容器内不要顺手做全量apt-get upgrade,那会破坏镜像一致性。
后面这条最该记住。
我把 11 个库和unzip都装进了容器,它们全部活在容器的可写层里,不在镜像里。docker restart时都还在,不用处理;但容器重建(改运行环境、compose up、升级镜像)会重置文件系统,补的东西全丢,必须重放。
我一开始判断错了,以为unzip不依赖任何库、重建后仍在。错的。unzip同样是apt-get装的,容器重建时文件系统重置,补的库和unzip一起消失。真正要区分的是操作类型,不是这个包有没有依赖。
所以「装双通道」的真实价值是互为备份、以及restart场景免维护,不是抗重建。抗重建的唯一正解是把补库命令存档,重建后重放一次。建议你现在就在仓库里放一个scripts/php-ext-replay.sh,把上面那段补库命令原封不动存进去,下次容器一重建,一行命令恢复。
四个坑排一下。面板勾了不等于扩展生效,一眼判据是php -m里有Unable to load dynamic library。.so在、共享库不在,判据是ldd xxx.so | grep "not found"有输出。扩展清单别凭经验列,去查composer.lock,而且要分清packages与packages-dev。容器内补装不抗重建,重建后重放脚本即可复原。
第 3 条我想再说一次:ext-iconv是本次最容易误判的一个,它出现在 lock 里、看着像硬依赖,实际在packages-dev段,composer install --no-dev时压根不校验。不看段号就下结论,是这类排查里最典型的翻车点。
下一个坑埋在同一个 PHP 运行环境里——看起来像故障、其实完全正常的回显。get_loaded_extensions(true)只返回一个Zend OPcache,是不是扩展都没了?php -i | grep 'Opcode Caching'显示Disabled,opcache 到底开没开?docker exec里cat /opt/1panel/.../php.ini报No such file,文件丢了?三个答案都是没坏,下一篇逐个拆。