一、先给结论:最小可用配置长什么样

Nginx 跑 WordPress 的"最简可用"配置只需要满足四件事:能解析 PHP、能处理伪静态规则、能正确返回 404 而不是 500、能配合 CDN 或缓存插件工作。 其余都是优化。
先给出这份配置骨架,后面每节解释为什么这么写:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/html;
index index.php index.html;
# 1. 静态资源直接由 Nginx 返回,不进 PHP
location ^~ /wp-content/uploads/ {
expires 30d;
access_log off;
add_header Cache-Control "public";
}
location ^~ /wp-content/themes/ {
expires 7d;
access_log off;
add_header Cache-Control "public";
}
location ^~ /wp-content/plugins/ {
expires 7d;
access_log off;
add_header Cache-Control "public";
}
# 2. 禁止访问敏感文件
location ~ /\.ht {
deny all;
}
location ~ /\.(well-known|git) {
deny all;
}
# 3. 伪静态规则
location / {
try_files $uri $uri/ /index.php?$args;
}
# 4. PHP 处理
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 300;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
}
# 5. 拒绝访问其他 PHP 文件
location ~ \.php$ {
deny all;
}
}
注意最后一个 location 块,这是安全配置里最关键的一条。 它的作用是禁止直接访问 .php 文件,只允许通过 index.php 这个入口。原因是 WordPress 的上传目录允许上传文件,如果有人上传了一个 shell.php 并且能直接通过 URL 访问,那服务器就沦陷了。WordPress 官方安全团队明确建议加上这条规则。
不过上面这份配置有个顺序问题:两个 location ~ \.php$ 块并存时,Nginx 只用第一个匹配的,所以第二个 deny 规则不会生效。 正确写法是用 location ~ \.php$ 配合 try_files 判断文件名是否存在:
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 300;
}
或者用带条件的正则,把不允许直接访问的目录排除掉:
location ~ ^/(wp-content/uploads|wp-includes)/.*\.php$ {
deny all;
}
这两种写法都是社区里最常见的错误来源,下面第八节会详细讲怎么排查。
二、php-fpm对接:socket还是端口,参数怎么定

Nginx 和 PHP-FPM 有两种通信方式,选错了会有一系列诡异问题。
Unix Socket(推荐):
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
TCP 端口:
fastcgi_pass 127.0.0.1:9000;
Unix Socket 的优势在于两点: 一是省掉一次 TCP 握手的开销(每次请求少几十毫秒的量级,对高并发站点累积起来明显);二是它走的是本地文件系统权限,天然只对本机开放,安全性更好。除非你的 PHP-FPM 在独立机器上,否则一律用 Socket。
切换方式是编辑 PHP-FPM 的池配置:
# /etc/php/8.2/fpm/pool.d/www.conf
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
改完重启 PHP-FPM,然后确认 Nginx 的用户有权限访问这个 socket:
# 确认 socket 文件权限
ls -l /run/php/php8.2-fpm.sock
# 确认 Nginx 运行用户
ps aux | grep nginx | head -1
如果出现 502 且日志是 "Permission denied" 或者 "connect() failed (2: No such file or directory)",九成是这里的问题。 前者是用户不匹配,后者是路径写错(Debian 上路径是 /run/php/php8.2-fpm.sock,Ubuntu 上可能是 /var/run/php/php8.2-fpm.sock,虽然 /var/run 是 /run 的软链接,但拼错版本号就是找不到)。
PHP-FPM 的进程数配置是另一个关键点。WordPress 站点上最常 OOM 的就是这一项。PHP-FPM 默认的 pm = dynamic 模式下,每个进程常驻的内存大约 30 MB 到 60 MB(取决于加载了多少扩展),这个数字比很多人以为的大得多。
按内存倒推进程数:
# /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500
估算公式:PHP 进程数 = (可用内存的 60%) 除以 单进程平均内存。 可用内存要扣掉系统和 Nginx 本身。
给几组实测参考值(每进程按 60 MB 算):
| 机器内存 | 扣掉系统后可用 | 建议 max_children | 说明 |
|---|---|---|---|
| 1 GB | 约 400 MB | 4 | 只够跑静态页加少量动态请求 |
| 2 GB | 约 1.2 GB | 10 到 12 | 单站常规配置 |
| 4 GB | 约 2.8 GB | 20 到 24 | 有 WooCommerce 时取下限 |
| 8 GB | 约 6 GB | 40 以上 | 多站点或高并发 |
pm.max_children 设过高的后果非常明确:机器被 OOM Killer 干掉。 而且 OOM Killer 挑中的进程不一定是 PHP-FPM,可能是 Nginx 甚至 sshd,表现为"网站突然连不上"但日志里什么都没有。
pm.max_requests 这个参数值得单独说。 它限制单个 PHP 进程处理多少个请求后被回收重建。设成 500 或 1000 的作用是定期清理进程里的内存泄漏——WordPress 的某些插件会随运行时间增长而积累内存,不重启进程就永远不释放。这个参数设为 0(不限制)是很常见的错误配置。
还有三个影响性能的参数:
# 每个进程处理多少请求后重启(防内存泄漏)
pm.max_requests = 500
# PHP 进程空闲多久后回收
pm.process_idle_timeout = 10s
# 启动时先启动几个进程
pm.start_servers = 2
改完必须重启才生效:
sudo systemctl restart php8.2-fpm
sudo systemctl reload nginx
三、伪静态:WordPress为什么需要它

WordPress 的默认 URL 是 ?p=123 这种形式,很不好看也不利于 SEO。开启"固定链接"(Permalink)后,URL 会变成 /2026/11/05/hello-world/ 这样的路径形式。
问题在于:Nginx 收到这个请求时,服务器上并没有这个目录也没有这个文件——它是 WordPress 动态生成的。所以需要一条规则告诉 Nginx:"这个路径如果找不到对应文件,就交给 index.php 去处理"。
这就是 try_files:
location / {
try_files $uri $uri/ /index.php?$args;
}
这条规则逐个尝试三个目标: 第一,查找字面路径 $uri 对应的静态文件(比如 /wp-content/uploads/2026/11/photo.jpg,命中就直接返回,不进 PHP);第二,查找目录 $uri/;第三,都不命中就用 /index.php?$args 交给 WordPress。
$args 不能丢。 很多教程写的 try_files $uri $uri/ /index.php; 会丢掉查询字符串,导致带参数的链接(分页 ?paged=2、搜索 ?s=关键词)失效。这是一个非常隐蔽的 bug:首页正常,翻到第二页就 404 或者搜索功能失灵。
WordPress 的完整伪静态规则(放在 server 块里,用于处理那些真正需要的路径):
# WordPress 默认规则
location / {
try_files $uri $uri/ /index.php?$args;
}
# 如果用的是目录形式(/hello-world/ 而不是 /index.php/hello-world/),上面这条就够
# 如果是带路径参数的写法,需要额外一条
location ~ ^/(.+/)?(wp-.*) {
rewrite ^/([^\.]+)/(.+)/(.*) /index.php?p=$2&/$3 last;
}
判断你的 WordPress 需要哪条规则的方法: 去后台的"设置 → 固定链接"里看当前设置。如果是"文章名"或"分类和文章名",try_files 那条就够了;如果是"自定义结构"里带路径参数的,那就要用上面第二条。
一个必须知道的坑:WordPress 的 rewrite 规则依赖 $_SERVER['REQUEST_URI'] 和 $_SERVER['PHP_SELF']。 如果你把 PHP-FPM 的 fastcgi_param 配错(比如漏了 REQUEST_URI),会出现"后台能进、前台首页 404、伪静态链接全部失效"的症状。检查方法:
# 看当前实际传给 PHP 的参数
sudo grep -r "fastcgi_param" /etc/nginx/fastcgi_params
这些参数一个都不能少:REQUEST_URI、QUERY_STRING、SCRIPT_NAME、PATH_INFO、DOCUMENT_ROOT、SERVER_PROTOCOL、REQUEST_METHOD、SERVER_NAME、SERVER_PORT。
四、静态资源加速:三个层次的做法

WordPress 的性能问题里,静态资源占了很大比重。三层加速各解决不同的问题。
第一层:直接由 Nginx 返回,跳过 PHP。 这是配置里 location ^~ /wp-content/uploads/ 那些块的作用。用 ^~ 而不是 ~ 是有讲究的:~ 是正则匹配,^~ 是前缀匹配且优先级更高。用 ^~ 可以保证这些路径不会被后面任何正则 location 抢走(否则上传目录里的 .php 文件可能落到 PHP 处理块里被执行,这是安全风险的来源)。
第二层:开启压缩。 文本类资源(HTML、JS、CSS、JSON、SVG)压缩率很高;图片、视频已经是压缩格式,再压是浪费 CPU。
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
Brotli 比 gzip 更好,前提是 Nginx 编译时带 --with-http_brotli_module。如果用的是 Nginx 官方仓库的稳定版,通常没有这个模块,需要自己编译或者用 nginx.org 的官方仓库。
brotli on;
brotli_comp_level 5;
brotli_static on;
brotli_types
text/plain
text/css
text/xml
application/javascript
application/json
image/svg+xml;
Brotli 相比 gzip 的两个优势: 压缩率通常高 15% 到 20%(对 JS/CSS 尤其明显),并且静态资源可以用 brotli_static on 直接读取预先压缩好的 .br 文件,运行时零 CPU 开销。
第三层:HTTP/2 或 HTTP/3。 WordPress 页面通常有 20 到 40 个资源请求,HTTP/1.1 下一个页面要建 20 到 40 次连接(虽然浏览器会并发 6 个),HTTP/2 下全部复用一条连接。
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
...
}
HTTP/3(基于 QUIC)的实际收益有限,因为它需要 UDP 443 端口,很多防火墙和云安全组默认不放行。除非你的用户中有相当比例遇到 HTTP/2 的队头阻塞问题,否则开 HTTP/2 就够了。
还有一个针对 WordPress 特别有效的做法:去掉不必要的资源。 每减少一个资源请求,页面就少一次往返。常见的减法有:
- 移除没用的 emoji 脚本(WordPress 会加载 wp-emoji-release.min.js,用 disable_emojis 过滤器去掉)。
- 移除页面里没用的 jQuery 迁移脚本。
- 把小图标合并成雪碧图或者内联成 data URI。
- 把 CSS 合并成一个文件。
这些通常靠插件或主题实现,比在 Nginx 层调优见效快得多。 一个经验数字:WordPress 页面如果从 30 个资源请求降到 12 个,在带宽 100 Mbps 的机器上首屏时间能差出 1 秒以上。
五、缓存:Nginx层与WordPress层各管什么

WordPress 的缓存有多个层次,容易搞混。划清边界的方法是问一个问题:这个缓存谁负责失效?
Nginx 层适合缓存的是"与请求参数无关的静态文件",也就是图片、脚本、样式、字体。这类文件的失效条件是"文件内容变了",用 expires 指令声明即可:
location ^~ /wp-content/uploads/ {
expires 30d;
access_log off;
add_header Cache-Control "public";
}
location ^~ /wp-content/themes/ {
expires 7d;
access_log off;
add_header Cache-Control "public";
}
但这里有个 WordPress 特有的问题:wp-content/themes/ 和 wp-content/plugins/ 里的文件名通常不带内容指纹。 主题更新一次、插件升级一次,文件名不变但内容变了,用户会拿到旧版本。
解决办法有两个。 第一个是给这些目录设短 TTL(比如 1 天而不是 7 天),牺牲一点命中率换正确性。第二个是用 WordPress 的版本号做查询参数,比如主题的样式表路径带 ?ver=5.2,Nginx 的缓存键会把它算进去——但这需要缓存键包含 query string,而 CDN 默认会忽略某些参数,两者配合要小心。
WordPress 层适合缓存的是"页面本身",用缓存插件实现(最常见的是把整页 HTML 存成静态文件)。这类缓存的失效条件是"内容更新了",所以缓存插件需要挂钩文章保存事件来清理缓存。
两层的分工是:Nginx 层缓存静态资源(浏览器和 CDN 也各有一份),WordPress 层缓存页面 HTML。 两者不冲突。
如果你的站点前面有 CDN,缓存键的选择就变得关键。 WordPress 的页面 URL 上可能带 ?p=123、?page_id=45 这类参数,这些参数不能加进 CDN 的忽略列表——加了就不同文章共用一个缓存键,页面内容直接串掉。而 utm_source 这类跟踪参数则应该加进去。完整清单在本站《国内CDN与海外VPS怎么配合》一文里有列。
顺便说一句页面缓存的常见问题:缓存插件和 CDN 同时开启时的"双重缓存"会让更新不生效。 表现是"我改了文章,刷新还是旧内容,清缓存就好了"。原因通常是有两层缓存都在 10 分钟的 TTL 上。排查办法是分别关掉其中一层,看是哪一层在起作用。
六、HTTPS与HTTP/2:证书和跳转的正确姿势

给站点上 HTTPS 的完整流程,顺序不能乱。
第一步:申请证书。 用 certbot 最省事:
sudo apt install -y certbot python3-certbot-nginx
# Nginx 方式(自动改配置)
sudo certbot --nginx -d example.com -d www.example.com
注意 certbot --nginx 会自动改你的 Nginx 配置,改之前先备份:
sudo cp /etc/nginx/sites-available/example.com /etc/nginx/sites-available/example.com.bak
第二步:检查自动续期。 certbot 的 systemd timer 默认是每天跑两次(12 点和 24 点各一次),但要确认 timer 是启用状态:
# 确认定时器已启用
sudo systemctl list-timers | grep certbot
# 手动测试续期(dry-run 不会真的申请)
sudo certbot renew --dry-run
第三步:强制 HTTPS 跳转。 在 80 端口的 server 块里做跳转,并且必须先让 ACME 校验路径能通过 HTTP 访问:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# ACME 校验路径保持可访问
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
}
# 其余全部跳转
location / {
return 301 https://$host$request_uri;
}
}
第四步:HSTS。 确认所有子域名都能走 HTTPS 之后再加:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
先别急着加 includeSubDomains。 这个参数一旦生效,所有子域名都会被强制 HTTPS,包括那些你还没上证书的(比如内部的监控域名、测试环境)。建议先只开 max-age=31536000 跑一周,确认没问题再加子域名参数。
第五步:混合内容。 页面里如果还有 http:// 开头的资源(图片、脚本、iframe),浏览器会拦截或者警告。排查方法:
# 在页面源码里找混合内容
curl -s https://example.com/ | grep -o 'http://[^"'"'"' ]*' | sort -u
把数据库里和主题里的 http 链接批量改成 https:
# WordPress 后台或 wp-config.php 里加
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
在 Nginx 层做强制替换(治标,但有效):
sub_filter 'http://example.com' 'https://example.com';
sub_filter_once off;
sub_filter 需要 ngx_http_sub_module 模块,官方仓库的 Nginx 都带。
七、502排查:按错误类型逐个击破

502 Bad Gateway 的含义是:Nginx 收到了请求,但转发给后端(PHP-FPM)时失败了。 排查一定要从 Nginx 错误日志入手,不要瞎猜。
第一步:看错误日志。
sudo tail -100 /var/log/nginx/error.log
第二步:按日志里的具体错误对号入座。
错误一:connect() failed (111: Connection refused) while connecting to upstream。 含义是 Nginx 连不上 PHP-FPM 的 socket 或端口。三个原因:
# 原因1:PHP-FPM 没运行
sudo systemctl status php8.2-fpm
# 原因2:socket 路径写错
sudo ls -l /run/php/
# 确认实际文件名与 Nginx 配置里的一致
# 原因3:用户权限不对
ls -l /run/php/php8.2-fpm.sock
# 确认 owner 是 www-data,Nginx 也以 www-data 运行
错误二:connect() failed (2: No such file or directory) while connecting to upstream。 同样是连不上,但原因几乎肯定是路径错。Debian 的路径是 /run/php/php8.2-fpm.sock,Ubuntu 可能是 /var/run/php/php8.2-fpm.sock。用 ls 确认真实路径再改配置。
错误三:Permission denied while connecting to upstream。 用户不匹配。修法是改 PHP-FPM 池配置里的 listen.owner 和 listen.group:
# /etc/php/8.2/fpm/pool.d/www.conf
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
改完 sudo systemctl restart php8.2-fpm。
错误四:upstream sent too big header while reading response header from upstream。 意思是 PHP 返回的响应头太大。常见于某个插件在 header 里塞了大量数据。解决方向是调大 Nginx 的缓冲:
fastcgi_buffer_size 64k;
fastcgi_buffers 8 64k;
错误五:upstream timed out (110: Connection timed out) while reading response header from upstream。 PHP 处理超时。改超时时间:
fastcgi_read_timeout 300;
错误六:no live upstreams。 PHP-FPM 的 pool 被配置成了 pm = ondemand 且没有可用进程。这个模式会在没有请求时回收所有进程,高并发下起进程来不及,导致间歇性 502。生产环境应该用 pm = dynamic。
第五步:绕过 Nginx 直接测 PHP-FPM。 这一步能立刻区分"是 Nginx 的问题还是 PHP 的问题":
# 用 cgi-fcgi 直接请求 PHP-FPM(需要装 libfcgi-bin)
sudo apt install -y libfcgi-bin
SCRIPT_FILENAME=/var/www/html/index.php \
SCRIPT_NAME=/index.php \
REQUEST_METHOD=GET \
QUERY_STRING= \
REQUEST_URI=/ \
DOCUMENT_ROOT=/var/www/html \
SERVER_PROTOCOL=HTTP/1.1 \
REDIRECT_STATUS=200 \
cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock
能返回 HTML 说明 PHP-FPM 本身没问题,问题在 Nginx 配置;返回空或者报错说明问题在 PHP 侧。
这一步能省下大量时间,值得记住。
顺便说一个容易混淆的点:502 和 504 的区别。 502 是"连上了但出错",504 是"等太久没等到"。如果你的 WordPress 偶尔出现 504 而不是 502,通常是某个操作(大批量导入、插件批量任务、后台更新)执行时间超过了 fastcgi_read_timeout。把超时调到 300 秒并重启,同时检查是不是有插件在做超时长的任务。
八、常见报错与配置错误对照表

这一节把最常见的现象、原因和处理方式列成表,遇到问题直接查。
| 现象 | 根本原因 | 处理方式 |
|---|---|---|
| 首页正常,后台 404 | rewrite 规则缺失或 try_files 写错 |
确认 /index.php?$args 那条路径写对 |
| 首页能开,文章页全 404 | 伪静态没启用 | 后台固定链接设为非默认,然后检查 Nginx rewrite |
| 翻页或搜索失效 | try_files 丢了 $args |
改成 /index.php?$args |
| 502 且日志是 Connection refused | PHP-FPM 没运行 | systemctl status php8.2-fpm |
| 502 且日志是 No such file | socket 路径错 | ls /run/php/ 确认真实文件名 |
| 502 且日志是 Permission denied | socket 权限不匹配 | 改 listen.owner/group 为 www-data |
| 间歇性 502 | pm = ondemand |
改成 pm = dynamic |
| 404 是 Nginx 的,不是 PHP 的 | 请求被 Nginx 的 location 拦了 | curl -I 看响应头里的 Server 和 X-Powered- |
| 上传目录里的 php 能被访问 | 缺 ^~ 前缀或缺 deny 规则 |
给 uploads 加 location ^~ 并 deny |
| CSS 样式不生效 | 静态文件被当成 PHP 处理 | 检查 location 优先级,^~ 优先 |
| 静态文件 404 | root 路径错 | 确认 root 和 WordPress 实际目录一致 |
| 上传大文件失败 | client_max_body_size 默认 1M |
改成 client_max_body_size 64m; |
| 502 大小写不一致 | 站点 URL 配置了两种 | 统一 server_name 或做 301 |
| 修改 .htaccess 无效 | Nginx 不读 .htaccess | 用 try_files 实现等价规则 |
| wp-admin 样式丢失 | 路径含大写或域名不一致 | 检查 WP_HOME/WP_SITEURL |
| 内存耗尽被 OOM 杀掉 | max_children 设太大 | 降低到 (可用内存 60%)/单进程内存 |
| 站点慢但 CPU 不高 | 磁盘 IO 瓶颈 | 检查 iostat 等待队列 |
其中三个最值得单独强调。
第一,.htaccess 在 Nginx 上完全无效。 这是从 Apache 迁移过来的人最常犯的错。Nginx 不读 .htaccess 文件。如果你在 WordPress 里用 Apache 的 .htaccess 做伪静态或重定向,Nginx 上一个都不会生效。 必须把规则翻译成 Nginx 的 location 配置。
第二,client_max_body_size 的默认值是 1 MB。 WordPress 后台的媒体上传超过 1 MB 就会失败,症状是"上传到一半卡住"或者返回 413。解法是在 server 块里加:
client_max_body_size 64m;
第三,"修改内容不生效"要先分清是哪一层缓存。 排查顺序:先在浏览器里强制刷新(Ctrl+Shift+R)排除浏览器缓存;再用 curl -sI https://example.com/ | grep -i "x-cache\|age\|cache-control" 看 CDN 缓存;然后看 WordPress 的页面缓存插件有没有生效;最后才怀疑 PHP 缓存(比如 opcache 的 opcache.validate_timestamps 设成了 0)。
一个必须做的确认:网站 URL 配置是否唯一。 检查 wp-config.php 里的 WP_HOME 和 WP_SITEURL,以及数据库 wp_options 表的 siteurl 和 home 字段。这四个值必须完全一致,包括协议和 www 前缀,否则会出现"部分资源 404"或者"跳转到错误域名"的诡异问题。
九、内存不同的机器该怎么配

给三档配置的具体数字,可以直接照抄。
1 GB 内存的单站点:
# /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 4
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 400
# Nginx
worker_processes auto;
worker_connections 512;
client_max_body_size 32m;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
gzip_comp_level 5;
这个配置的定位是"能跑",不是"跑得好"。 1 GB 机器上不该装 WooCommerce、不该开对象缓存、不该有多个站点。
2 GB 内存的标准站点:
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
worker_processes auto;
worker_connections 1024;
client_max_body_size 64m;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
gzip_comp_level 6;
4 GB 内存带 WooCommerce 的站点:
pm = dynamic
pm.max_children = 16
pm.start_servers = 4
pm.min_spare_servers = 3
pm.max_spare_spare_servers = 8
pm.max_requests = 500
WooCommerce 的后台页面(订单列表、商品列表)特别吃 PHP 内存。 单个请求可能跑到 100 MB 以上,所以这里要用单进程内存 80 MB 来估算,不能用 60 MB,max_children 要往低调。如果 WooCommerce 站点出现 502,先怀疑是不是 max_children 太高导致 OOM。
判断内存设得合不合适的实测方法:
# 看 PHP-FPM 进程的实际内存占用
ps -o pid,rss,cmd -C php-fpm8.2 | head -20
# 看 8.2-fpm 的 RSS 列(单位 KB)
ps aux --sort=-rss | grep php-fpm | head -5
# 看系统有没有发生 OOM
sudo dmesg -T | grep -i "out of memory\|killed process"
dmesg 里如果有 "Killed process ... php-fpm",就是 max_children 设太高的直接证据。
还有一点关于 PHP-FPM 的 worker 数量与 CPU 的关系。 pm.start_servers 不要设得比 CPU 核数多太多。PHP 处理请求是 CPU 密集型(WordPress 尤其如此,大量数据库查询和正则),CPU 是瓶颈时增加进程数只会增加上下文切换。
十、WordPress本身的问题:哪些不是Nginx的锅

有一类问题会被误判成"Nginx 配置有问题",实际根源在 WordPress 或 PHP 内部。这一节把最容易混淆的几类拆开。
第一类:后台操作超时。 导入文章、批量更新、生成缩略图这类操作跑很久,最后显示"Maximum execution time exceeded"。这和 Nginx 的 fastcgi_read_timeout 是两个不同的超时。 PHP 侧的超时由 max_execution_time 控制,默认 30 秒(CLI 下是 0,不限)。
# /etc/php/8.2/fpm/php.ini
max_execution_time = 300
max_input_time = 300
max_input_vars = 3000
改完重启 PHP-FPM。max_input_vars 默认是 1000,这个限制经常被忽略:一个文章里有超过 1000 个表单字段,或者导入插件一次提交太多数据,就会报 "Undefined array key" 或者数据被截断。
第二类:数据库连接数超限。 症状是后台间歇性出现 "Too many connections" 或者 "MySQL server has gone away"。WordPress 的每个 PHP 请求都可能开一个数据库连接,在 pm.max_children 较高时,连接数会等于 PHP 进程数加上一些固定连接。
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
max_connections = 100
innodb_buffer_pool_size = 256M
innodb_buffer_pool_size 是 MySQL 最重要的内存参数,它决定 InnoDB 能把多少数据页缓存在内存里。默认值通常是 128 MB,在 2 GB 内存的机器上偏小。经验值是物理内存的 50% 到 60%(如果这台机器只跑 MySQL);如果还跑着 PHP 和 Nginx,则取 30% 左右。
第三类:WordPress 的定时任务没跑。 WordPress 用 wp-cron.php 做定时任务(发布定时文章、清理回收站、发邮件)。默认是访客访问页面时顺便触发,这意味着没有访客时定时任务就不执行。
解法是用系统 cron 定时调用:
# 每 5 分钟触发一次 WordPress cron
*/5 * * * * curl -s -o /dev/null https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1
注意这条 cron 和 Nginx 无关,它走的是完整的 HTTP 请求。 如果你的站点有 CDN 拦截或者 WAF 拦 POST 请求,cron 会被挡掉。用上面这种"外部触发"的方式还能顺便绕过这个问题,因为外部触发不需要站点能给自己发请求。
第四类:opcache 没开或者配置不对。 OPcache 能把 PHP 编译结果缓存在内存里,对 WordPress 这种文件数量多的系统提升明显。
# /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.max_accelerated_files 必须大于你实际加载的 PHP 文件数。 WordPress 加上插件主题,文件数轻松过千,默认值 10000 一般够用,但装了重插件的站可能不够。
opcache.validate_timestamps 是那个"改了代码不生效"的常见原因。 设成 0 之后,PHP 永久信任缓存里的编译结果,你改了代码但 opcache 不重新编译,页面还是旧逻辑。生产环境设 0 配手动 opcache_reset 是常见做法,开发环境必须保持 1。
# 改完代码后手动重置
sudo systemctl reload php8.2-fpm
第五类:主题或插件的循环依赖。 WordPress 的钩子系统在某些插件组合下会互相触发,形成死循环,表现是页面加载到一半卡住、CPU 100%。这不是 Nginx 的问题,但 Nginx 会把它放大成 504 超时。
排查用 PHP 的慢日志:
# /etc/php/8.2/fpm/php.ini
; slowlog = /var/log/php-fpm/slow.log
; request_slowlog_timeout = 5s
第六类:数据库慢查询。 WordPress 的文章表在数据量大了以后,缺索引的查询会非常慢。开启慢查询日志:
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
然后看日志里反复出现的语句,针对 wp_options 表的 option_name 和 wp_postmeta 表的 meta_key 建索引,这两个是 WordPress 慢查询的重灾区。
第七类:文件系统权限问题。 症状是"后台点保存提示失败"或者"上传提示无法写入"。检查:
# 目录需要可写,文件需要属主正确
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;
但不要在有 wp-config.php 的目录上无脑执行 chmod -R 777。 777 让任何本地进程都能改你的配置,是常见的安全事故起点。目录 755、文件 644 是正确组合。
十一、安全相关的配置检查清单

Nginx 跑 WordPress 的安全配置,挨个确认。
第一,隐藏版本号。 版本号泄露能让攻击者直接查已知漏洞。
server_tokens off;
第二,禁止访问敏感文件。 包括 .ht、.git、备份文件、数据库文件:
location ~ /\.(ht|git) {
deny all;
}
location ~* \.(bak|sql|log|ini|sh)$ {
deny all;
}
location ~* \.(env|yml|yaml)$ {
deny all;
}
注意 \.env 这条。 WordPress 生态里有些插件(尤其是那些"一键配置"的)会在根目录留下 .env 文件,里面可能存着数据库密码和 API 密钥。
第三,上传目录禁止执行 PHP。 这是最重要的一条:
location ^~ /wp-content/uploads/ {
expires 30d;
access_log off;
add_header Cache-Control "public";
location ~ \.php$ {
deny all;
}
}
第四,限制 PHP 的危险函数。 在 PHP-FPM 池配置里:
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
php_admin_value[open_basedir] = /var/www/html/:/tmp/:/var/log/
open_basedir 这个限制很有用,它把 PHP 能访问的目录限制在站点目录内,即使有文件被上传了并被执行,攻击者也没法读取 /etc/passwd 里的密码哈希,也没法写到站点目录外面。
第五,限制请求体大小和请求频率。 防止恶意请求打满磁盘和 CPU:
client_max_body_size 32m;
client_body_timeout 30s;
client_header_timeout 15s;
limit_req_zone $binary_remote_addr zone=wpzone:10m rate=10r/s;
第六,数据库和后台的访问控制。 最简单的做法是用 Nginx 的 allow/deny 限制 wp-admin 的访问来源:
location ^~ /wp-admin/ {
allow 1.2.3.4; # 你的固定 IP
deny all;
try_files $uri $uri/ /index.php?$args;
}
如果你的 IP 经常变,用 Basic Auth 加一层更实际:
location ^~ /wp-admin/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ /index.php?$args;
}
第七,定期更新。 WordPress 核心、插件、主题都要及时打补丁:
# WordPress 核心自动更新
sudo chown -R www-data:www-data /var/www/html
让 WordPress 的自动更新能写文件,需要文件属主是 www-data。 这一条是新手装完站点后最常忘的,也是"后台点了更新,提示失败"或者"更新到一半白屏"的根本原因。
第八,别忘了 Nginx 本身的配置语法检查和重载。 改完配置先检查再重载,避免把线上的服务搞挂:
sudo nginx -t # 语法检查
sudo systemctl reload nginx # 检查通过后再重载
nginx -t 这一步不要省。 它检查不通过时 reload 会失败,但如果你用 restart 而不是 reload,服务会直接停掉起不来。
按你的情况,下一步该看哪家
上面讲的是判断标准,但多数人最后还是想问一句「那我该买谁」。按你的实际情况对照下面几条,直接去看对应的机型页比继续比价更快:
- 只想搭个站、预算优先:先看价格最低那档够不够用,别为用不上的配置付钱。看 iwebfusion 当前机型与价格
- 只是开发测试、临时用:按小时或月付,用完就删,别买年付。看 racknerd 当前机型与价格
价格与库存以商家页面当日显示为准,下单前建议先确认退款窗口与是否支持按月付费。
十二、常见问题

Nginx 报 "unknown directive" 怎么办? 说明配置里用了一个当前 Nginx 没编译进去的模块。常见的有 brotli、sub_filter、headers_more。检查编译参数:
nginx -V 2>&1 | tr ' ' '\n' | grep with-
如果确实缺模块,要么去掉相关配置,要么换用官方 nginx.org 仓库重装,要么自己编译。
502 但 PHP-FPM 明明在运行,怎么排查? 按这个顺序:看 error.log 的具体错误串(连不上、权限、还是超时);用 curl 直接打 Nginx 的 PHP location 确认是否 502;用 cgi-fcgi 直连 PHP-FPM 判断是 PHP 侧还是 Nginx 侧;最后检查 Nginx 运行用户对 socket 有没有权限。
上传图片提示 413,怎么解决? client_max_body_size 默认只有 1 MB,在 server 块里改成 64m 并 reload。改完还不行就检查 PHP 的 upload_max_filesize 和 post_max_size(默认都是 2M),三者要协调,PHP 的值不能小于 Nginx 的值。
WordPress 后台白屏了,怎么快速定位? 这是 PHP 致命错误。先开 WP_DEBUG:
# wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
然后看 wp-content/debug.log。如果是空白页且 debug.log 里没有记录,多半是 PHP-FPM 直接崩了或者 OOM 被杀了,去 dmesg 里确认。
伪静态规则怎么确认生效了? 在 WordPress 后台把固定链接改成"文章名",然后访问一篇不存在的文章,正确的结果是 WordPress 的 404 页面(HTTP 404),而不是 Nginx 的 404 页面。区分方法看响应体内容:Nginx 的 404 是它自己的页面,WordPress 的 404 有你站点的样式。
.htaccess 里的规则怎么迁移到 Nginx? Apache 的 RewriteRule 要翻译成 Nginx 的 rewrite 或 location 指令。常用的几条对应关系:RewriteRule ^index\.php$ - [L] 对应 try_files;RewriteRule ^(.*)\.html$ /$1.php [L] 对应 location ~ \.html$ { rewrite ^(.*)\.html$ /$1.php last; }。最稳妥的做法是装一个 WordPress 插件导出 Nginx 规则,或者直接用 try_files 这条万能规则。
改了 Nginx 配置后网站 502,reload 也没用? 可能是配置有语法错误导致 reload 失败,Nginx 还在跑旧配置(这其实是好事)。查 systemctl status nginx 和 journalctl -u nginx 看 reload 是否真的成功。永远先 nginx -t 再 reload。
静态文件缓存设了 30 天,用户看不到更新怎么办? 两种解法:给文件名加内容指纹(推荐),或者在 WordPress 里用版本号查询参数(?ver=1.2.3),Nginx 的缓存键会区分不同版本。记得同步检查 CDN 的缓存键配置,源站改了但 CDN 缓存着旧版本的情况更常见。
Nginx 怎么限制某个 IP 的访问速度? 用 limit_req_zone 定义区域,然后在 location 里应用:
limit_req_zone $binary_remote_addr zone=slow:10m rate=5r/s;
location /wp-login.php {
limit_req zone=slow burst=10 nodelay;
try_files $uri $uri/ /index.php?$args;
}
给 wp-login.php 加上这个,能挡住大部分暴力破解。
WordPress 后台打开很慢,但前台页面正常,为什么? 后台每个页面要加载几十个插件的 JS 和 CSS,而且后台不做页面缓存,前台通常有缓存插件兜着。用 curl -s -o /dev/null -w "%{time_total}\n" https://example.com/wp-admin/ 测一下,如果超过 2 秒,逐个停用插件定位。另外检查一下后台有没有挂 WooCommerce 订单列表这类吃内存的页面。
站点用 https 打开时图片裂了,怎么定位? 页面里引用了 http 的资源就是混合内容。查法:
curl -s https://example.com/ | grep -o 'http://[^"'"'"' ]*' | sort -u | head -20
找到之后在 WordPress 的数据库里跑一次替换:
# 用 wp-cli 批量替换
wp search-replace 'http://example.com' 'https://example.com' --all-tables
改完记得清缓存,否则 CDN 和页面缓存插件还会吐出旧内容。
WordPress 首页打开很慢,后台却正常,怎么查? 这种情况通常是首页被缓存插件缓存了一个坏版本,或者有大量文章导致首页的数据库查询慢。先做两步:临时禁用页面缓存插件看是否恢复;再开 MySQL 慢查询日志看是不是 wp_posts 的查询没走索引。
Nginx 日志里大量 499 是什么? 499 是 Nginx 特有的状态码,含义是"客户端主动关闭了连接"。常见于用户点了刷新就退出了、或者客户端的请求还没读完就断开。如果你在做压测或者看到 499 很多但 502 为 0,一般不是服务端问题,可以忽略。但如果 499 伴随 502 同时出现,就要检查 client_body_timeout 是不是太短。
怎么知道我的 Nginx 配置有没有生效? 两个方法:一是 nginx -T 把当前生效的完整配置打印出来(比 cat 某个文件准,因为它包含所有 include 的文件);二是看响应头里的 Server 字段,server_tokens off 之后它会显示成 nginx 而不是 nginx/1.24.0。

喜欢
高兴
鬼脸
呵呵
无聊
伤心