1Panel 部署 ThinkPHP8 踩坑实录

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 网络互通。

为了让命令能直接复制,我先在终端里定义几个变量,后续命令全部复用:

bash 复制代码
# 换成你在 1Panel 里给 PHP 运行环境起的名字(= 容器名)
export PHP_CTN="php84"
# 换成你在 1Panel 里给 MySQL 起的名字
export MYSQL_CTN="mysql84"

# 确认两个容器都在跑
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" | grep -E "$PHP_CTN|$MYSQL_CTN"

下文所有回显都是我在自己服务器上跑出来的真实输出,实测于 2026-09-16 和 09-17。为了让命令通用,我把回显里的容器名统一替换成了上面的变量名,除此之外一字未改。

1Panel 的 PHP 扩展页勾选后,最终会落到容器的 php -m 上。所以验证动作只有一条,别信面板,去问容器:

bash 复制代码
# 一次筛出本项目关心的扩展 + opcache/zip,看有没有加载告警
docker exec -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 能不能成功。面板状态和实际加载结果,是完全脱钩的两件事。

整条加载过程长这样:
#mermaid-svg-PGXchGIf3t5CNIIK{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-PGXchGIf3t5CNIIK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PGXchGIf3t5CNIIK .error-icon{fill:#552222;}#mermaid-svg-PGXchGIf3t5CNIIK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PGXchGIf3t5CNIIK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PGXchGIf3t5CNIIK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PGXchGIf3t5CNIIK .marker.cross{stroke:#333333;}#mermaid-svg-PGXchGIf3t5CNIIK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PGXchGIf3t5CNIIK p{margin:0;}#mermaid-svg-PGXchGIf3t5CNIIK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PGXchGIf3t5CNIIK .cluster-label text{fill:#333;}#mermaid-svg-PGXchGIf3t5CNIIK .cluster-label span{color:#333;}#mermaid-svg-PGXchGIf3t5CNIIK .cluster-label span p{background-color:transparent;}#mermaid-svg-PGXchGIf3t5CNIIK .label text,#mermaid-svg-PGXchGIf3t5CNIIK span{fill:#333;color:#333;}#mermaid-svg-PGXchGIf3t5CNIIK .node rect,#mermaid-svg-PGXchGIf3t5CNIIK .node circle,#mermaid-svg-PGXchGIf3t5CNIIK .node ellipse,#mermaid-svg-PGXchGIf3t5CNIIK .node polygon,#mermaid-svg-PGXchGIf3t5CNIIK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PGXchGIf3t5CNIIK .rough-node .label text,#mermaid-svg-PGXchGIf3t5CNIIK .node .label text,#mermaid-svg-PGXchGIf3t5CNIIK .image-shape .label,#mermaid-svg-PGXchGIf3t5CNIIK .icon-shape .label{text-anchor:middle;}#mermaid-svg-PGXchGIf3t5CNIIK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PGXchGIf3t5CNIIK .rough-node .label,#mermaid-svg-PGXchGIf3t5CNIIK .node .label,#mermaid-svg-PGXchGIf3t5CNIIK .image-shape .label,#mermaid-svg-PGXchGIf3t5CNIIK .icon-shape .label{text-align:center;}#mermaid-svg-PGXchGIf3t5CNIIK .node.clickable{cursor:pointer;}#mermaid-svg-PGXchGIf3t5CNIIK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PGXchGIf3t5CNIIK .arrowheadPath{fill:#333333;}#mermaid-svg-PGXchGIf3t5CNIIK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PGXchGIf3t5CNIIK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PGXchGIf3t5CNIIK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PGXchGIf3t5CNIIK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PGXchGIf3t5CNIIK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PGXchGIf3t5CNIIK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PGXchGIf3t5CNIIK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PGXchGIf3t5CNIIK .cluster text{fill:#333;}#mermaid-svg-PGXchGIf3t5CNIIK .cluster span{color:#333;}#mermaid-svg-PGXchGIf3t5CNIIK 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-PGXchGIf3t5CNIIK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PGXchGIf3t5CNIIK rect.text{fill:none;stroke-width:0;}#mermaid-svg-PGXchGIf3t5CNIIK .icon-shape,#mermaid-svg-PGXchGIf3t5CNIIK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PGXchGIf3t5CNIIK .icon-shape p,#mermaid-svg-PGXchGIf3t5CNIIK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PGXchGIf3t5CNIIK .icon-shape .label rect,#mermaid-svg-PGXchGIf3t5CNIIK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PGXchGIf3t5CNIIK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PGXchGIf3t5CNIIK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PGXchGIf3t5CNIIK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存在
缺失
1Panel 面板勾选扩展

界面显示:已启用
生成 conf.d/docker-php-ext-xxx.ini
容器启动,php-fpm 读取 ini
dlopen 加载 xxx.so
该 .so 依赖的共享库

在基础镜像里存在吗?
扩展加载成功

php -m 中可见该模块
扩展加载失败

Unable to load dynamic library

面板只校验第 1 步的勾选状态,第 4 步的 dlopen 结果它根本管不到。

不要猜缺哪个库。ldd 会把一个二进制依赖的动态库逐个列出来,缺的标 not found:

bash 复制代码
# 逐个扩展跑 ldd,只挑出「找不到」的库
docker exec -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 是一份比「我印象里需要什么」可靠得多的扩展清单。

bash 复制代码
# 在项目根目录,列出 lock 里所有 ext-* 平台依赖
grep -n '"ext-' composer.lock

但这里有个很多人会漏的分辨点:composer.lock 里有两个段,packages(生产)和 packages-dev(开发)。生产部署走的是 composer install --no-dev,packages-dev 整个不会被装,也就不会被校验。

我写了个小脚本把两个段拆开看(已实跑):

php 复制代码
<?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 只说包要求什么,还得看我的代码用什么。直接搜函数名:

bash 复制代码
# 高精度计算(bcmath 提供的是 bcadd/bcsub/bcmul/bcdiv/bccomp 等函数)
grep -rn "bcsub\|bcadd\|bcmul\|bcdiv\|bccomp" app

# 图像处理(gd)与国际化(intl)的典型调用
grep -rn "imagecreatefrom\|getimagesize" app
grep -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_mysql

iconv 属于开发段要求、且是 PHP 默认编译项(镜像自带),核验一下不吃亏,但不必为它专门做什么。

清单定完,动作就剩下取舍。我这次的路线是全救,11 个库全补、4 个扩展全留,理由是第六节那个坑:动扩展勾选可能触发容器重建。

补库命令用的是自适配写法,Debian 13 上有几个包名处于过渡期,所以对同义词做逐个尝试:

bash 复制代码
docker exec -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
'
docker restart "$PHP_CTN"

顺序很重要。如果你打算走「只留需要的扩展」这条路,先在 1Panel 改扩展列表,之后再到容器里 apt-get。反过来做,改扩展时一旦触发容器重建,你刚补的库会被一起冲掉,白干一次。

复验只有一条命令:

bash 复制代码
# 有告警就会有输出;空输出 = 全部加载成功
docker exec -i "$PHP_CTN" php -m 2>&1 | grep -i 'Unable to load'

回显(实测 2026-09-17):

复制代码
(无输出)

Day 1 这里躺着 4 条 Warning,现在空了。再打一次全量确认:

bash 复制代码
docker exec -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 OPcache

gd、intl、memcached、zip 全部出现在列表里,48 个 PHP 模块加 1 个 Zend 模块,零告警。

顺带把解包能力补成双通道。原因在第 2 节那个 Warning 里:zip 扩展依赖 6 个容器内 apt-get 装的库,链条长、易断;而 unzip 是独立二进制,装上就基本不会挂。两条路互为备份,Composer 解压 dist 包时哪条通走哪条:

bash 复制代码
# 补装 + 复验
docker exec -i "$PHP_CTN" bash -lc 'apt-get update -qq && apt-get install -y --no-install-recommends unzip'
docker exec -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,文件丢了?三个答案都是没坏,下一篇逐个拆。

相关推荐
anew___1 小时前
《从零手写操作系统 (21):用户态C运行时——从裸ELF到printf》
linux·服务器·c语言
jackletter1 小时前
linux:systemd之守护进程
linux·运维·服务器·systemd
用户4107192013252 小时前
VMware 虚拟机 Ubuntu 无法连接网络
linux
酬谢神明则必安2 小时前
git学习记录01
linux·git·学习
Ruiery3 小时前
Linux 6.6内核 CPU 深度解析(五):CPU 空闲状态机 cpuidle
linux·运维·服务器
殷色玫瑰3 小时前
C语言编译和链接:从 .c到 .exe,程序到底经历了什么?
linux·c语言·c++·算法
菜鸟~noob2333 小时前
【太空电子战】午夜锤行动中的电磁静默区:可逆干扰的实战验证【含matlab代码】
linux·开发语言·matlab·电子战
Fcy6484 小时前
Linux下 传输层协议TCP详解
linux·网络·tcp/ip
刘胡子大叔5 小时前
grep -n 行号辨换行
ubuntu