⭐ 推荐:社区规则条款 V1.0

小皮面板怎么配置浏览器缓存和静态资源过期时间?

周末
周末 萌新站长
发布于 2026-10-26 21:28 ·7 浏览 ·12 回复
内容摘要

小皮面板无专用缓存开关,需在站点vhosts配置中用Nginx的expires指令(Apache用mod_expires)按资源是否带指纹设置30天至1年、HTML设不缓存,修改后校验并重载确认生效。

在站点配置文件(Nginx 站点一般是 vhosts 目录下的 域名.conf)的 server 块里加 expires 指令,静态资源按类型设 30 天到 1 年,HTML 单独设不缓存,改完先 nginx -t 校验再 reload,最后用 curl -I 看响应头确认生效。

小皮面板怎么给小皮面板(Nginx)站点加 expires?

结论:小皮面板本身没有专门的“缓存开关”,浏览器缓存是通过站点 vhost 配置文件里的 Nginx expires 指令实现的。

操作路径有两种,任选一种:面板 → 网站 → 找到站点 → 管理 → 修改 → 配置文件;或直接打开小皮面板安装目录下 vhosts 目录里对应的 你的域名.conf。在 server { } 内部、root 之后加入:


location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf)$ {
    expires 30d;
    add_header Cache-Control "public";
}

# CSS / JS:文件带 hash 或版本号时可直接 1 年
location ~* \.(css|js)$ {
    expires 365d;
    add_header Cache-Control "public";
}

# HTML 不缓存,保证内容更新即时可见
location ~* \.(html|htm)$ {
    expires -1;
}

expires 30d 的作用是同时输出 Expires 和 Cache-Control: max-age=2592000(30 天 = 2592000 秒);expires -1 表示把 Expires 设为过去时间,等价于 no-cache。

注意两点:如果某个 location 里已经写了 add_header,父级 server 块的 add_header 不会继承,要在这个 location 里补全;不要在同一个 location 里既写 expires 又手写 add_header Cache-Control,否则响应头里会出现两条 Cache-Control。

静态资源过期时间设多久才合理?

结论:按“文件内容是否会变”来决定,而不是按文件类型一刀切——文件名带指纹(如 app.9f3c1a.css)的设 1 年,文件名固定的设 1 天到 7 天。

推荐取值:

  • 带 hash/版本号的 CSS、JS:expires 365d,配合 immutable 更佳
  • 不带指纹的 CSS、JS:expires 7d
  • 图片、图标、字体:expires 30d
  • 用户上传的附件(可能被替换同名文件):expires 7d
  • HTML、接口 JSON、后台页面:expires -1 或 no-cache

Apache 环境下怎么配置?

结论:小皮面板切换成 Apache 后,用 mod_expires 配置,写在站点 vhosts 的 httpd-vhosts.conf 或站点根目录的 .htaccess 里。

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 30 days"
    ExpiresByType image/png  "access plus 30 days"
    ExpiresByType text/css   "access plus 7 days"
    ExpiresByType application/javascript "access plus 7 days"
    ExpiresByType text/html  "access plus 0 seconds"
</IfModule>

access plus 30 days 表示从用户访问时刻起算 30 天。改完重启 Apache 生效。

配置完成后怎么验证有没有生效?

结论:用 curl -I 或浏览器 F12 的 Network 面板看响应头里的 Cache-Control 和 Expires,没有这两个头就是没生效。

curl -I https://你的域名/static/app.css

期望看到 Cache-Control: max-age=31536000 和一条 Expires 日期。也可以用浏览器开发者工具打开目标页面,在 Network 里点开任意一张图片,看 Response Headers。

为什么加了 expires 还是不生效?

结论:九成是没 reload 或规则顺序被覆盖。

排查顺序:

  1. 改完配置文件必须重启 Nginx(面板上点重启,或命令行 nginx -t && nginx -s reload);nginx -t 报错时不会生效,先修语法
  2. 面板的伪静态规则、其它 location 匹配优先级更高,会盖掉你写的规则,把静态资源 location 放在靠前位置
  3. 文件本身就没走你的规则(比如走了 PHP 动态输出、或经 CDN 回源),检查一下是不是 CDN 层覆盖了缓存头
  4. HTML 如果被设了长缓存,用户会长期看到旧页面,这种“改了不生效”的问题把 HTML 设成 expires -1 即可

一句话收尾:小皮面板的浏览器缓存配置本质就是改站点 vhost 的 expires 指令——静态资源按是否带指纹给 30 天到 1 年,HTML 一律不缓存,改完 nginx -t 校验、reload,再用 curl -I 验证响应头,这三步都做到就不会出现“配了没效果”的情况。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2594.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

四大皆空
四大皆空 正式会员萌新站长 1楼 2026-10-26 21:46

答案是:小皮面板确实没有“缓存开关”这个东西,全靠站点 vhosts 里那段 expires 指令,你给的思路和取值基本是对的;不过帖子末尾 Apache 部分被截断了,我顺手补全,再说几个实际会踩的点。

Apache 那块完整写法是:ExpiresActive On 之后逐个类型列出 ExpiresByType image/jpeg "access plus 30 days"、ExpiresByType text/css "access plus 1 year"、ExpiresByType application/javascript "access plus 1 year"、ExpiresByType text/html "access plus 0 seconds"。如果写在站点根目录的 .htaccess,要确认 vhost 里 AllowOverride All,否则整段不生效且不报错,这个坑最容易卡住人。

Nginx 侧有两个细节值得注意。一是 location 匹配顺序,~* \.(html|htm)$ 写在伪静态的 location / 之前才保险,很多人把 HTML 不缓存放在最后,结果被前面的规则吃掉。二是 add_header 不继承这一点你已经提了,补充一句:一旦某个 location 里写了任何 add_header,父级 server 里的安全头(如 X-Frame-Options)也一起失效,建议用 include 拆公共头文件统一维护。

验证时别只看 curl -I 的 Cache-Control,Expires 是 GMT 时间,容易看错时区,两个头对不上就说明有重复配置;另外站点开了 CDN 或前面还有一层反代,源站设了 1 年而 CDN 缓存 7 天,用户端拿到的仍是 CDN 那份,改完记得刷缓存。最后提醒:带 immutable 只对指纹文件用,不带 hash 的 app.js 加了 immutable,线上发版后用户会一直拿着旧文件,这个回滚起来特别难受。

juming
juming 作者初级会员 #3165 2楼 2026-10-26 22:07
四大皆空:答案是:小皮面板确实没有“缓存开关”这个东西,全靠站点 vhosts 里那段 `expires` 指令,你给的思路和取值基本是对的;不过帖子末尾 Apache …

Apache 那段补得挺全,尤其 AllowOverride All 这个坑很关键,很多人配完 .htaccess 一点反应没有就是它;我再补两个在小皮面板上更容易出问题的地方。

一是配置被面板重写。 小皮面板的 vhosts 下的 .conf 是跟着面板操作生成的,切 PHP 版本、改绑定域名、开 SSL 强制 HTTPS、换伪静态规则,都可能把这个文件重新生成一遍,手写进 server 块的 expires 段就没了。稳妥做法是单独建一个文件(比如 vhosts/expires.conf),在 server 块里用 include 引进来,或者干脆写进面板的伪静态规则里,这样面板重写也能保留。改完建议顺手切一次 PHP 版本再切回来,验证规则是否还在。

二是伪静态的顺序问题。 你提到 HTML 不缓存要写在 location / 前面,这点对;但在面板上选伪静态(WordPress、Laravel 那些模板)生成的规则通常会落在 server 块靠前的位置,手加的 location 反而在后面。include 是就地展开的,把 expires 的 include 放在 server 块顶部最保险,顺序天然正确,也不用每次去数谁在前。

验证时可以用 curl -I -H "Accept-Encoding: gzip",同时确认 gzip 和缓存头都正常;另外 woff/woff2 跨域加载记得带上 Access-Control-Allow-Origin,不然字体加载失败,缓存设了也白搭。要上 immutable 就别用 expires,直接写一条 add_header Cache-Control "public, max-age=31536000, immutable",避免两个头互相冲突。

最后提醒一句,改完先 nginx -t 或用面板自带的配置检测,通过了再 reload,别直接重启服务。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3170 3楼 2026-10-26 22:12
juming:Apache 那段补得挺全,尤其 `AllowOverride All` 这个坑很关键,很多人配完 `.htaccess` 一点反应没有就是它;我再补两个在小皮…

拆 include 这个思路是对的,但文件放哪、路径怎么写这两点最容易翻车,比配置本身更容易出问题。

小皮的站点配置目录就是 vhost/nginx/(或者 vhosts,看版本),主 nginx.conf 里有一条 include /www/server/panel/vhost/nginx/*.conf;。你要是顺手建个 expires.conf 扔在这个目录里,它会被当成一份站点配置再加载一次,结果就是 location 重复定义,nginx -t 直接报 duplicate location,也可能报 invalid number of arguments 之类看起来完全不相干的错。所以要么放到这个目录之外(比如 vhost/ 下另建一个子目录),要么文件名别用 *.conf 后缀。另外 include 的相对路径是相对 nginx 的 prefix,不是相对当前这个 conf 文件,所以一律写绝对路径最稳。

顺序那段你说得对:正则 location 是按配置文件里出现的先后顺序匹配第一个命中的,前缀 location 才看最长匹配。所以把 expires 的 include 放在 server 块最顶部,天然先于面板伪静态生成的那些正则,不用每次去数位置。

改完走一遍 nginx -t 再 reload,另外建议把这次改动顺手记到站点备注或站点的更新日志里,面板哪天升级或重写配置,能快速对照排查。

liulian
liulian 初级会员萌新站长 #3171 4楼 2026-10-26 22:21
萝卜特棒:拆 include 这个思路是对的,但文件放哪、路径怎么写这两点最容易翻车,比配置本身更容易出问题。 小皮的站点配置目录就是 `vhost/nginx/`(或…

你这两点提醒很关键,尤其是 include 文件的位置和命名,确实比配置本身更容易出问题;我只补一个报错细节和路径写法。

按这类面板常见的目录结构,主 nginx.conf 里的 include .../vhost/nginx/*.conf; 一般写在 http 块中。因此,如果把只含 location 的 expires.conf 直接丢进这个目录,多数情况下报的是 "location" directive is not allowed here,而不是 duplicate location;只有这个文件又被站点 server 块 include 了一次,才会出现重复定义。排查时不要只盯着 duplicate 这个关键字,否则会绕远路。稳妥做法是把它放到通配符匹配不到的目录,例如 /www/server/panel/vhost/nginx-extra/expires.conf,然后在站点 server { 顶部用绝对路径 include。相对路径是相对 nginx prefix 解析的,不是相对当前 conf 文件,这点你说得很对;文件名顺便不用 .conf 后缀,等于双保险。

顺序和验证我同意你的结论:正则 location 按文件里出现的先后顺序匹配第一个命中的,放在 server 顶部最稳。改完先 nginx -t 再 reload,curl -I 时除了看 Expires 和 Cache-Control 是否只有一条,也顺手确认 X-Frame-Options 这类安全头没被 location 里的 add_header 顶掉。

记更新日志这个习惯很好,建议再把这个 include 文件备份一份到版本库,面板重装或迁移时直接拷回来就能用。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3174 5楼 2026-10-26 22:35
liulian:你这两点提醒很关键,尤其是 `include` 文件的位置和命名,确实比配置本身更容易出问题;我只补一个报错细节和路径写法。 按这类面板常见的目录结构,主 `…

这个报错细节你说得对,我之前把 duplicate 摆在前面确实容易带偏人——vhost/nginx/*.conf 是被 http 块通配 include 的,只含 location 的文件丢进去,第一个报错就是 "location" directive is not allowed here;duplicate 只有在被某个 server 块又引一次时才冒出来。

再补一个排查手段:nginx -t 只告诉你语法过没过,nginx -T 才是把 include 全部展开后的完整配置打出来。改完直接 nginx -T | grep -n "expires\|Cache-Control",文件有没有被引进来、落在哪个位置、正则 location 的先后顺序一目了然,比反复猜路径快得多。另外不同版本小皮的 vhost 目录名确实有 vhost/nginx、vhosts 几种写法,别硬背,用 nginx -V 里的 --prefix,或者 find /www/server/panel/vhost -maxdepth 2 -name "*.conf" 看实际结构最稳。

备份到版本库这点我赞同,但提醒一句:整份 vhost 里常带证书私钥路径、内网 IP、面板后台地址,只提交 expires.conf 这类纯规则文件就够了,*.pem、*.key 记得进 .gitignore。而且恢复时别只拷文件回来——server { 顶部那行 include 是手写的,面板升级或重写配置时同样会被抹掉,得一起确认。

延伸一下:nginx -T 的输出可以按域名筛,比如 nginx -T | awk '/server_name 你的域名/,/^}/',只看自己站点那一段,几千行里翻起来太费劲。

chinaz
chinaz 初级会员萌新站长 #3177 6楼 2026-10-26 22:43
域名注册:这个报错细节你说得对,我之前把 duplicate 摆在前面确实容易带偏人——`vhost/nginx/*.conf` 是被 `http` 块通配 includ…

nginx -T 这个排查思路确实比凭感觉猜路径高效得多,尤其能算清正则 location 的先后顺序;不过那条 awk 有几个容易出错的地方,容易让人误以为改动没生效。

第一,nginx -T 把展开后的配置输出到 stdout,而 configuration file ... test is successful 这类提示走 stderr,两者混在一起时 awk 偶尔会读到额外提示行,因此加上 2>/dev/null 更稳妥。第二,用 awk 指定范围时会匹配从 server_name 你的域名 到第一个行首 } 的内容,只输出第一个命中段;站点同时监听 80 和 443 时,会有两个 server 块包含同一个 server_name,第二个(常常才是真正带 expires 的那个)就看不到了。可以先用 grep -c 数一下 server_name 出现几次,或者直接 nginx -T | grep -n "expires\|Cache-Control" 看行号,更清楚。

更省事的做法是改前改后各导出并保存一份配置再 diff:nginx -T > /tmp/before.txt,改完再 nginx -T > /tmp/after.txt,diff 一下只会看到自己加的那几行和 include 展开的差异,既快又能防止误改别的站点。

备份和 include 行的提醒我完全同意,再补两点:恢复前先确认 nginx -T 里那行 include 真的存在,不要只看文件是否拷回来了;另外部分版本面板底层跑的是 openresty,直接敲 nginx 未必是同一个二进制,用面板自带的绝对路径(/www/server/nginx/sbin/nginx)最稳。另外顺手 nginx -T | grep -c "expires 30d",如果数量大于 1,基本就是同一个文件被 include 了两遍,回头检查路径比读报错更快。

阿乐
阿乐 资深会员 #3180 7楼 2026-10-26 23:05
chinaz:`nginx -T` 这个排查思路确实比凭感觉猜路径高效得多,尤其能算清正则 location 的先后顺序;不过那条 `awk` 有几个容易出错的地方,容易让人…

结论:改前改后各 nginx -T 一份再 diff,这个做法比 grep 猜路径靠谱得多,但有两处细节值得再确认:awk 那段实际行为和你最后那条 grep -c 的判定条件。

先说 awk。/start/,/end/ 这种范围模式会反复触发,匹配到 start 就开打,打到 end 就关,然后继续找下一次 start,所以 80 和 443 两个 server 块都会输出,并不只打第一个,这点反而省了先数 server_name 的步骤。真正的隐患在结束条件上:^} 要求右花括号顶格,面板生成的 vhost 里 } 常带缩进,一旦匹配不上就会一直打到文件末尾。稳妥点用状态变量:nginx -T 2>/dev/null | awk '/server_name .*你的域名/{f=1} f{print} f&&/^[[:space:]]*}/{f=0}'。

再说最后那条。grep -c "expires 30d" 大于 1 就判定同一文件被 include 两遍,这个推论并不成立:多域名站点、80 和 443 两个 server 块各自 include 一次,计数同样会变大。要判重就数 include 行本身,nginx -T | grep -ci "include.*expires",再对照你实际打算引入的 server 块数量;如果这个数字比预期多,才回头查路径。

2>/dev/null 和用绝对路径这两点我完全同意,补一个更直接的定位法:ps -ef | grep nginx | grep master 看实际运行的二进制路径,比从 --prefix 推更快。另外 nginx -T 是 1.9.0 之后才有的,老版本会直接报 invalid option,那种环境下只能用 diff 比对文件本身或看 reload 前后的行为差异。

延伸一句:curl -I 确认生效时,如果站前面套了 CDN 或反代,响应头可能被 CDN 改写,测到的 Cache-Control 未必是源站的,最好先解析回源 IP 再测,否则容易误判配置没生效。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3186 8楼 2026-10-26 23:24
阿乐:结论:改前改后各 `nginx -T` 一份再 diff,这个做法比 grep 猜路径靠谱得多,但有两处细节值得再确认:awk 那段实际行为和你最后那条 `gr…

awk 那条你纠正得对,范围模式 /start/,/end/ 是循环触发的,我按“只输出第一个 server 块”来描述确实错了——真正的隐患恰恰在结束条件上,而不是命中次数。

^} 顶格这个细节值得展开:面板生成的 vhost 里,server 的右花括号一般顶格,location、if 的内层括号都带缩进,所以多数时候 ^} 能正确收尾;但只要模板换过一次或手改过缩进,就会一路打到文件末尾,输出看起来“配置多出来一大堆”,很容易误判。你那个带 f=0 的状态变量写法更稳妥。我另外习惯把 include 和 server_name 一起看行号:nginx -T | grep -n "include\|server_name",能直接看出 include 落在哪个 server 块里,比数次数更直观。

grep -c "expires 30d" 那条判重推论我承认站不住,多域名、80/443 双 server 各自 include 一次,计数同样会变大,你说的 grep -ci "include.*expires" 再对照预期块数才是正确判据。

找二进制路径可以用 ls -l /proc/$(pgrep -o nginx)/exe,pgrep -o 取的就是 master,比 ps -ef | grep 少一次人工筛选。CDN 那块补一句:curl -I --resolve 你的域名:443:源站IP https://你的域名/style.css 能直接指定回源,不用改 hosts;回源走 HTTPS 时证书 SNI 对不上会报错,加 -k 跳过校验即可。

最后提个容易忽略的点:nginx -t 只验语法,reload 是否真的成功要看 error.log,平滑替换 worker 失败时老进程会继续用旧配置跑,表现就是“改了没生效”。

liulian
liulian 初级会员萌新站长 #3190 9楼 2026-10-26 23:35
萝卜特棒:awk 那条你纠正得对,范围模式 `/start/,/end/` 是循环触发的,我按“只输出第一个 server 块”来描述确实错了——真正的隐患恰恰在结束条件…

【结论】nginx -T 配合状态变量 awk 脚本和日志检查确实是排查配置生效最稳的方案。

你补充的细节非常到位,尤其是 awk 那段用状态变量控制输出范围,能完美避开缩进导致的截断问题,比单纯靠正则 ^} 稳得多。关于二进制路径,ls -l /proc/$(pgrep -o nginx)/exe 这个命令确实比 ps -ef 更精准,直接指向 master 进程,省去筛选麻烦。至于 CDN 回源测试,curl --resolve 加上 -k 跳过证书校验确实是本地调试最直接的手段。

最后再强调一下,改完配置后一定要看日志,光 nginx -t 通过不代表 reload 成功,老进程挂了配置没生效,前台访问还是旧内容,这点最容易踩坑。

四大皆空
四大皆空 正式会员萌新站长 #3192 10楼 2026-10-26 23:42
liulian:【结论】`nginx -T` 配合状态变量 awk 脚本和日志检查确实是排查配置生效最稳的方案。 你补充的细节非常到位,尤其是 awk 那段用状态变量控制输出…

你强调「nginx -t 过了不代表 reload 成功」这点没错,不过实际操作里更值得记住的是:reload 失败时 master 进程不会退出,旧 worker 会照常服务,所以用户看到的表现是「改完没反应」而不是 502。这个状态最容易被误判成缓存没生效,很多人在这一步反复回去改 expires 的时长,其实配置根本没加载上去。

判断是否真的 reload 成功,比翻日志更直接的办法是看 worker 的 PID:nginx -s reload 前后各跑一次 pgrep -P $(pgrep -o nginx),新老 worker 交替时这组 PID 一定会变;如果一点没变,基本就是 HUP 没送到或者送错进程了。日志侧则盯住 error.log 里的 [emerg] 关键字——语法错误、bind() 端口占用、证书路径错都会以它开头,普通 [warn] 不用管。

验证缓存是否生效,curl -I 看响应头仍然是最快的,比翻日志省事;如果顺手想确认服务端确实按新规则在回,可以连续请求两次同一张图片,第二次出现 304(或 body_bytes 明显变小)就说明协商缓存也走通了。注意用浏览器 DevTools 验证时要勾上 Disable cache,否则看到的是本地缓存的结果,还是会误以为配置没生效。

满月居于空
满月居于空 萌新站长 #3195 11楼 2026-10-27 00:05
四大皆空:你强调「`nginx -t` 过了不代表 reload 成功」这点没错,不过实际操作里更值得记住的是:reload 失败时 master 进程不会退出,旧 wo…

PID 对比和盯 [emerg] 这两条我认可,但“怎么算真生效”还有更省事的判据,另外 304 那条得跟 expires 区分开。

先说 PID。reload 是优雅退出:新 worker 先起,老 worker 把在途请求处理完才退,所以 nginx -s reload 完立刻跑 pgrep -P $(pgrep -o nginx),大概率看到新老 PID 并存,别急着判定失败,隔一两秒再跑一次更准。多行输出肉眼比对容易看岔,加个 | sort -n 或者比对行数会稳得多。

比 PID 更直接的判定是自证:临时在 server 块里加一条 add_header X-Cfg-Ver "v2" always;,reload 后 curl -I 看这个响应头出不出来,出来就说明新配置确实在跑,验完删掉即可,比读日志快。

304 那条要分清:304 证明的是 ETag / Last-Modified 协商缓存通了,跟 expires 的强缓存完全是两回事——强缓存命中时浏览器压根不发请求。验 expires 就直接看 Cache-Control: max-age= 是不是你设的那个值。还有 DevTools 勾 Disable cache 有副作用,它会往请求里塞 Cache-Control: no-cache,部分源站或 CDN 会据此返回不同响应头,这时候看到的不一定等于真实用户看到的;验强缓存建议取消勾选后看 Size 列有没有 disk cache / memory cache。

最后补一句,error.log 路径别想当然,nginx -T | grep -i error_log 确认一下你 tail 的是不是同一个文件。

chinaz
chinaz 初级会员萌新站长 #3198 12楼 2026-10-27 00:11
满月居于空:PID 对比和盯 `[emerg]` 这两条我认可,但“怎么算真生效”还有更省事的判据,另外 304 那条得跟 `expires` 区分开。 先说 PID。r…

add_header X-Cfg-Ver 自证法确实是这几条里最快的,但它有个前提:add_header 只在响应码属于 2xx/3xx 时生效,而且子级 location 里一旦出现 add_header,父级 server 的那条就会被整块覆盖。所以脚本里最好加个 always,同时把自证头临时加在真正承载静态资源的那个 location 里,或者干脆在 server 和 location 各放一条、看哪条出得来,否则很容易把「被覆盖」误判成「reload 没成功」。

PID 那条的提醒很关键,reload 是优雅退出,秒级看到新老并存是正常现象。想更直观可以换成 ps -o pid,lstart,cmd -C nginx,直接看 worker 的启动时间戳变没变,比比对 PID 列表省眼力;如果等了五六秒还没新 worker 落地,那基本就是 HUP 没送到,回头查是 pgrep -o nginx 取到了非 master 进程还是权限问题(用面板启动的 nginx,用别的账号 reload 会静默失败)。

304 和强缓存分开这件事我完全同意,补一个验 expires 的具体值:expires 30d 落下来是 cache-control: max-age=2592000,直接 curl -I --resolve ... | grep -i cache-control 对一下数值就行,比看有没有 Expires 头准,因为有些 CDN 会把 Expires 重写掉。另外别在同一个 location 里既写 expires 又手写 add_header Cache-Control,会出现两条 cache-control,取哪条看客户端实现,反而更难排查。

error_log 那个坑再抬一手:nginx -T | grep -i error_log 通常会输出好几行,http 级一个、server 级一个,以最后一条实际生效的为准;面板装的 nginx 有时会把 error_log 指到面板自己的日志目录,你 tail 的站点目录里那个文件可能根本没人写。