这次部署是 2026 年 9 月 17 号做的。1Panel v2 面板,PHP 8.4.25 跑在容器里,镜像就是 1panel-php-fpm:8.4.25。框架是 ThinkPHP 8.1.4,鉴权用 firebase/php-jwt v7.1.1。MySQL 8.4.11 也在容器里,跟 PHP 挂在同一个容器网络上。
部署完成后打不开,通常有明确报错:502、PHP Fatal、连接被拒。这类问题好查,因为它有故障相。
.env 填错是另一回事。它的典型症状是这样:站点能打开,页面能渲染,接口能响应,但就是不对。或者反过来,所有登录接口全线失败,而界面上只显示一句进不去,看起来像前端 bug。
这类故障最费时间,因为它没有故障相。
下面按「填错会出什么症状」来组织,不按字段清单走。命令里的站点目录我都用变量代替,先在终端执行一次。
bash
export SITE_DIR="/opt/1panel/www/sites/<你的域名>/index"
这一篇里的配置项和结论,都来自这次真实部署,字段名和结构原样保留,域名、密码、密钥做了参数化。标着官方的部分来自 Composer 和 PHP-JWT 的发布说明与安全公告,出处随文标注。
.env 为什么总是不存在
它跟 vendor/ 是同一个原因,两件事叠在一起。一是 .gitignore 里写了它,git clone 下来的仓库天然不含。二是部署脚本的 rsync 排除清单里也写了它,同步不会覆盖它。
这两条都对。第二条本身还很有必要,否则每次更新代码都会把服务器上的配置清掉。
结果就是它永远不存在,而且永远不会自己出现。
所以唯一的来源是模板。
bash
cd "$SITE_DIR"
cp .example.env .env
但从模板复制,跟配置正确,是两件事。我当时也以为复制过来就算配好了,后来才发现模板是给开发机写的默认值,而你的部署拓扑是容器化的。下面三个值,就卡在这道缝里。
必查项一,DB_HOST,模板给的值就是错的
打开刚复制出来的 .env,会看到这一行。
ini
DB_HOST = 127.0.0.1
这在开发机上是对的,在容器部署里一定是错的。
PHP 跑在容器里,而 127.0.0.1 在容器内部指向的是容器自己,那儿没有任何 MySQL 在监听。数据库在另一个容器里,你甚至没把 3306 映射到宿主机,这正是正确的安全做法。
正确值该填 MySQL 容器的名字。
ini
DB_HOST = <MySQL 容器名>
这个容器名每台服务器随机生成,1Panel 不会给你一个通用值。所以它必须在上一步就记下来。
bash
# 安装 MySQL 后立刻记下容器名
docker ps --format "table {{.Names}}\t{{.Image}}" | grep -i mysql
容器之间能靠名字互通,是因为它们接在同一个 Docker 网络上。1Panel 默认建的那个网络叫 1panel-network。这也是为什么「不要开放 3306 到公网」和「容器名能连上数据库」这两件事可以同时成立。
症状:页面能打开,但数据库连不上。就是那个能打开却不对劲的形态。
核验一遍。
bash
cat .env | grep DB_HOST # 显示的必须是容器名,不是 127.0.0.1
显示的必须是容器名,不是 127.0.0.1。
必查项二,DB_PREFIX 必须留空
模板里这一行是这样的。
ini
DB_PREFIX =
它是空的,而它必须保持空。
不少人会按习惯给它填个前缀,比如 tp_,理由是表名统一管理更干净。但在当前的这个项目里,Model 用的是完整表名,也就是 v1_xxx 这种写法。前缀会被再拼一层,变成 tp_v1_xxx,而那张表不存在。
症状:模型一调用就报表不存在。
这类错误的隐蔽性在于,它只在第一次真正查库的时候才暴露。如果某个接口走的是缓存或静态数据,可能过很久才发现。
必查项三,APP_DEBUG 要关,但要留到最后再关
这条有正反两面,两面都重要。
正面:生产必须关。
ini
APP_DEBUG = false
开着的话,任何一次报错都会把数据库密码、密钥、完整调用栈渲染到页面上。这是直接泄露凭据。
反面:排障期不要先关。
这一点很多人搞反了。如果你在部署一开始就设成 false,那么之后每一次失败都只剩一句。
页面错误,请稍后再试
所有线索都没了,你会从能定位的问题退回靠猜。
所以正确的顺序是这样。部署与排障期设 true,需要看到真实报错与堆栈。验收通过后改回 false,生产不能泄露凭据。
先开,修完,再关。这是一个有始有终的动作,不是一次设置。
顺带一个更省事的做法。项目里预留一个公开、无需登录、且会顺带验证数据库连通性的健康检查接口。排障时它能一次回答两个问题:服务活着吗,数据库通吗。不用靠猜。
想确认 APP_DEBUG 到底关没关,故意触发一次错误,看回显里有没有泄露数据库密码。
硬约束:JWT 密钥必须大于等于 32 字节
这一条是硬性的运行时校验,不是建议。短了会让所有登录接口全线失败。
firebase/php-jwt 从 v7.0.0 起新增了 HMAC 密钥长度校验。官方发布说明给的是按算法分档的下限。HS256 要 32 字节,HS384 要 48 字节,HS512 要 64 字节。
为什么被迫从 6.x 升到 7.x
安全公告 CVE-2025-45769 针对的是 php-jwt v6.11.0 及以前。它归在 CWE-326,加密强度不足。公告指出,库不校验密钥长度。
有个细节值得知道。这个 CVE 在 NVD 上是 Disputed 状态,理由是密钥长度是应用的责任,不是库的责任。但 GitHub Advisory Database 保留了这条公告,编号 GHSA-2x45-7fc3-mxwq。原因是维护者确实发了补丁。结果是 composer audit 会拦截所有 php-jwt v6 的安装。
所以对使用者来说,争论谁对不重要,你装不上 6.x 了。
不足的下场是明确的。
vbnet
DomainException: Provided key is too short
症状:所有登录接口失败。小程序端表现为进不去,而前端只会显示一句失败提示,看起来像前端问题。
生成方式,以及模板注释里的那个坑
bash
# 用户端 JWT 密钥
openssl rand -hex 32
# 管理员端 JWT 密钥(再执行一次,得到不同的值)
openssl rand -hex 32
模板里的注释写的是「请生成 32 位随机字符串」。这里很容易被 32 这个数字绕进去。
openssl rand -hex N 产出的是 2N 个字符,1 个字节变成 2 个十六进制字符。
-hex 16 产出 32 个字符,长度恰好 32 字节。它能通过校验,但正好压在临界值上,零余量。
-hex 32 产出 64 个字符,也就是 64 字节。达标,而且留足了余量。
所以用 -hex 32,产出的字符串长度是 64 个字符。这两件事不冲突。32 在这两个地方分别指的是随机字节数,和要求的最小字节数。
两份密钥必须是不同的值
ini
JWT_SECRET = <第 1 个>
ADMIN_JWT_SECRET = <第 2 个>
用户端与管理员端走两套独立密钥。这是爆炸半径控制,万一其中一份泄露,另一侧不会同时失守。
所以别偷懒把两条命令的输出写成同一个值,那样等于把隔离白做了。把两个值放一起比对,它们不能相同。
收尾,权限收紧
.env 里全是凭据,权限要收。
bash
chmod 600 "$SITE_DIR/.env"
改完 .env 后,PHP-FPM 要不要重启取决于它的配置加载方式。多数情况下不需要,.env 是应用层在每次请求时读取的。如果发现改动不生效,再重启容器确认。
还有两个值顺手一起查。DB_CHARSET 要填 utf8mb4,填错了是中文乱码,判断依据就是跟库级字符集保持一致。.env 的权限要收到 600,收错了凭据可以被其他用户读取,用 ls -l .env 看一眼就知道。
但有一点要注意。在 1Panel 里改运行环境配置并保存,有概率触发容器重建而不是重启,那会把你之前补装进容器的依赖库清掉。改动 .env 走文件编辑,不要走面板的那个配置页。