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):

csharp 复制代码
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 里括号的内容:

python 复制代码
'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 能不能成功。面板状态和实际加载结果,是完全脱钩的两件事。

整条加载过程长这样:

flowchart TD A[&#34;1Panel 面板勾选扩展<br/>界面显示:已启用&#34;] --> B[&#34;生成 conf.d/docker-php-ext-xxx.ini&#34;] B --> C[&#34;容器启动,php-fpm 读取 ini&#34;] C --> D[&#34;dlopen 加载 xxx.so&#34;] D --> E{&#34;该 .so 依赖的共享库<br/>在基础镜像里存在吗?&#34;} E -->|存在| F[&#34;扩展加载成功<br/>php -m 中可见该模块&#34;] E -->|缺失| G[&#34;扩展加载失败<br/>Unable to load dynamic library&#34;] style A fill:#eaf2fd,stroke:#4a7fd4,color:#1f2328 style E fill:#fff8e1,stroke:#d4a72c,color:#1f2328 style F fill:#eaf7ee,stroke:#2da44e,color:#1a7f37 style G fill:#fdecec,stroke:#d1242f,color:#d1242f

面板只校验第 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'
ini 复制代码
--- 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 跑出来:

rust 复制代码
[生产段] 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:

rust 复制代码
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
css 复制代码
[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'
csharp 复制代码
/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,文件丢了?三个答案都是没坏,下一篇逐个拆。

相关推荐
金銀銅鐵1 小时前
[Java] 通过行为参数化传递代码
后端·函数式编程
浪遏1 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
明月_清风1 小时前
数据处理完放在哪里?一文搞懂数据仓库与数据湖
大数据·后端·数据分析
据说幸运很容易1 小时前
TestNG 分组接入现有框架:实现、踩坑与解法
后端·架构
IT_陈寒1 小时前
我的JavaScript代码为啥在forEach里没按预期执行?
前端·人工智能·后端
用户788477316341 小时前
源码交付清单:做完一个项目,你到底该从开发商手里拿到什么
后端
thissai1 小时前
Laravel 12 日志配置详解:config/logging.php 逐项说明
后端
mldong1 小时前
引擎里没有 setStatus:状态迁移收口,不用状态机框架
后端·架构
程序猿DD1 小时前
OctaFuse Gateway 2.12.0:供应商账号管理、路由工作区与多模态入口发现
后端