为什么有人从 Apache 换到 Nginx,结果网站反而挂了
很多个人站长的服务器最初都是「一键装环境」装上来的,默认带的是 Apache。跑了一两年,看到别人说 Nginx 更省内存、并发更高,于是照着一篇教程把 Apache 停了、Nginx 装上、配置文件复制过去——然后网站 500、图片 404、后台进不去,甚至连首页都打不开。最后只能灰溜溜地把 Apache 再启起来。
问题不在 Nginx,而在于 Apache 和 Nginx 处理请求的模型、配置的语法、以及「URL 到文件的映射方式」根本不是一回事。你以为你在「换一个引擎」,实际上你在换整套规则。这篇就把迁移过程中真正会出事的几个点讲透,包括 .htaccess 怎么翻译成 Nginx 的 location、伪静态规则怎么改写、以及迁移途中怎么做到不断服。
先搞清楚两者最本质的三个差异
如果你不理解差异,只会照着抄配置,出了错就完全没有排查方向。三个核心差异:
第一,进程模型不同。Apache 的 prefork 模式是「一个连接一个进程」,每个进程要吃掉几十 MB 内存,200 个并发连接就意味着 200 个进程;worker/event 模式好一些,但仍然是每个连接占一个线程。Nginx 是事件驱动的异步模型,一个 worker 进程能扛几千个连接,请求是「状态机」处理的,不需要为每个连接常驻资源。这就是为什么同样 1G 内存的小鸡,Apache 开了 100 并发就开始 swap,Nginx 却能轻松跑几千。
第二,配置作用域不同。Apache 允许在网站根目录放一个 .htaccess 文件,任何目录级别的 URL 改写、访问控制、压缩设置都能写在那里,Apache 每次请求都会去读一遍(AllowOverride All 时开销很大)。Nginx 完全没有 .htaccess 这个概念,所有规则必须写进主配置文件或者 include 进来的文件里,靠 location 块按前缀或正则匹配。这也是迁移中最痛的部分。
第三,URL 处理方式不同。Apache 的 mod_rewrite 是基于「请求路径 + 一组规则 + 重写标志」的流水线,一条规则改写后可以继续流向下一条。Nginx 的 rewrite 指令虽然语法类似正则,但它是在 location 匹配阶段执行的,而且会涉及 server / location 两个不同的重写上下文——放在 server 里的 rewrite 和放在 location 里的 rewrite,执行时机和 last/break 的效果完全不同。这是绝大多数人迁移翻车的地方。
迁移第一步:不是装 Nginx,而是先备份和核对资料
动手之前三件事必须做完,否则出事没有退路。
1. 完整备份现存配置和网站目录。
# 备份 Apache 配置
tar czf /root/apache-backup-$(date +%F).tar.gz /etc/apache2 /etc/httpd
# 备份网站目录(排除日志,减少体积)
tar czf /root/site-backup-$(date +%F).tar.gz \
--exclude='*.log' --exclude='logs' \
/var/www/html
# 备份数据库
mysqldump --single-transaction --routines --triggers \
--all-databases | gzip > /root/db-backup-$(date +%F).sql.gz2. 把现存站点的 URL 清单抓下来。这一步很多人会跳过,但它是迁移能否「不丢收录」的关键。用 curl 抓 sitemap,或者直接扫一遍网站目录,把所有的 URL 形态(含伪静态地址)记录到一个文件里。迁移完成后,用这个清单逐条去新站上验证返回码,凡是 404 的就是你的 rewrite 规则没翻译对。
# 从 sitemap 提取 URL 清单
curl -s https://www.example.com/sitemap.xml \
| grep -oE '<loc>[^<]+</loc>' \
| sed 's/<loc>//;s/<\/loc>//' > /root/urls.txt
wc -l /root/urls.txt3. 确认 Nginx 和 PHP-FPM 的 PHP 版本一致。Apache 一般用 mod_php,PHP 是作为 Apache 模块跑的;Nginx 不能内嵌 PHP,必须用 PHP-FPM 通过 FastCGI 通信。很多站点迁移后 502,就是因为 Nginx 连的 FPM socket 路径不对,或者 FPM 池监听的用户没有读网站目录的权限。
# 查看 Apache 当前用的 PHP 版本和加载的模块
apache2ctl -M | grep php
php -v
# 确认 PHP-FPM 池配置
ls /etc/php/*/fpm/pool.d/
grep -E '^(listen|user|group)' /etc/php/*/fpm/pool.d/www.conf第二步:把 .htaccess 里的伪静态规则翻译成 location
这是迁移的核心工作量。先找出所有 .htaccess:
find /var/www/html -name '.htaccess' -exec echo '=== {} ===' \; -exec cat {} \;然后逐条翻译。最常见的几类:
场景一:WordPress 的经典伪静态。Apache 侧是:
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]翻译成 Nginx:
location / {
try_files $uri $uri/ /index.php?$args;
}注意这里的精髓:Apache 的 !-f / !-d 两个条件,在 Nginx 里被 try_files 一句话取代——「先试文件,再试目录,都不存在就交给 index.php」。千万不要用 rewrite . /index.php last; 去硬转,那会让所有静态资源(css/js/图片)也走一遍 PHP,性能暴跌,而且容易死循环。
场景二:带查询参数的重写。Apache:
RewriteRule ^article/([0-9]+)\.html$ /article.php?id=$1 [L,QSA]Nginx:
rewrite ^/article/([0-9]+)\.html$ /article.php?id=$1 last;带 QSA(保留原查询串)时,Nginx 默认就会保留,除非用 ?$args 显式拼接或者末尾加 ? 丢弃。这一点是隐性的:如果你的规则末尾写成 ... last;,原请求的 ?page=2 会被丢掉;如果写成 ... ?id=$1 last; 结尾带问号,那么原参数被截断。
场景三:整站 301 跳转(http→https 或 裸域→www)。Apache:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]Nginx(推荐用两个 server 块,最清晰、最少坑):
server {
listen 80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name www.example.com;
# ...ssl 证书等配置...
}用 return 301 而不是 rewrite ... permanent,因为 return 不做正则匹配,开销更低,而且语义更明确。$request_uri 保留原始路径和查询串,比 $1 更不容易出错。
场景四:目录密码保护。Apache 的 .htaccess + .htpasswd:
AuthType Basic
AuthName "Restricted"
AuthUserFile /var/www/.htpasswd
Require valid-userNginx 里不能按目录放文件,必须写进 server 或 location:
location /admin/ {
auth_basic "Restricted";
auth_basic_user_file /var/www/.htpasswd;
}好消息是 .htpasswd 文件本身可以直接复用,Nginx 认识同一种格式(bcrypt/apr1 都支持)。生成可以用 htpasswd -c /var/www/.htpasswd username。
第三步:PHP-FPM 打通,别让 502 卡住你
Nginx 自己不会跑 PHP,必须把 .php 请求转给 PHP-FPM。一个能用的最小配置:
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}三个最容易忽视的点:
1. try_files $uri =404; 不能省。如果省掉,一个不存在的 /foo.php 请求会被转给 FPM,FPM 在找不到文件时会按照 cgi.fix_pathinfo 的配置去猜,历史上这直接导致过任意文件执行漏洞。加上 =404 就安全了。
2. SCRIPT_FILENAME 必须显式设置。fastcgi_params(注意不是 fastcgi.conf)里通常没有这一行,而 fastcgi.conf 里是写死的 $document_root$fastcgi_script_name。如果你 include 的是 fastcgi_params 又忘了补这一行,PHP 会说「No input file specified」或者「Access denied」。
3. socket 路径和权限。最常见的是 FPM 监听 unix:/run/php/php8.2-fpm.sock,但池配置里 listen.owner/listen.group 没设成 www-data,Nginx worker 连不上,报 502 Bad Gateway,error.log 里是 connect() to unix:... failed (13: Permission denied)。改完池配置记得 systemctl reload php8.2-fpm。
第四步:平滑切换,做到用户无感
最忌讳的切换方式是「停 Apache → 启 Nginx → 发现不对 → 停 Nginx → 启 Apache」,来回折腾期间网站全程不可用。正确做法是让 Nginx 先跑在 8080 端口,Apache 继续占着 80/443,两边同时在线对比:
# Nginx 先监听 8080
server {
listen 8080;
server_name _;
root /var/www/html;
# ...其余配置...
}然后做三件事:
1. 用 URL 清单批量对比。把之前抓下来的 URL 逐条在 8080 端口上请求,比较状态码和内容长度,与 80 端口上的 Apache 做差异对比:
while read url; do
old=$(curl -s -o /dev/null -w '%{http_code} %{size_download}' "$url")
new=$(curl -s -o /dev/null -w '%{http_code} %{size_download}' \
"${url/https:\/\/www.example.com/http://127.0.0.1:8080}")
[ "$old" != "$new" ] && echo "DIFF: $url | apache=$old | nginx=$new"
done < /root/urls.txt2. 把 Nginx 的日志和 Apache 的日志放在一起看。重点看 Nginx 的 error.log,任何 rewrite or internal redirection cycle、open() failed (2: No such file)、upstream prematurely closed 都是配置问题的信号。
3. 确认无误后一次性切端口。改 Nginx 监听 80/443,停 Apache,systemctl disable --now apache2。切换前后各留 10 分钟观察,确认 sitemap 里的 URL 全部 200。
第五步:迁移后必做的五项收尾检查
很多人迁移完就以为结束了,其实真正影响 SEO 和用户体验的东西在后面。
检查一:所有 URL 返回码。用清单批量查,任何非 200 的都要处理。301 是可以接受的(说明跳转链没断),404 必须修。
while read url; do
code=$(curl -s -o /dev/null -w '%{http_code}' -L "$url")
[ "$code" != "200" ] && echo "$code $url"
done < /root/urls.txt检查二:附件和图片能不能正常访问。Apache 对目录有 Options Indexes 的站点,换成 Nginx 后目录列表默认关闭(Nginx 需要显式 autoindex on;),这通常是好事。但要注意 Apache 的 Alias 映射在 Nginx 里对应 location /path/ { alias /real/path/; },alias 的路径结尾斜杠必须和 location 的斜杠一致,否则会出现路径拼接错误(比如 /uploads/1.jpg 被映射成 /real/path../1.jpg)。
检查三:上传目录的可写权限。Nginx 和 PHP-FPM 通常跑在 www-data(Debian/Ubuntu)或 nginx(CentOS)用户下。WordPress 上传失败、后台不能更新插件,基本都是目录属主不对:
chown -R www-data:www-data /var/www/html/wp-content/uploads
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;检查四:gzip 和静态资源缓存。Apache 用的是 mod_deflate + mod_expires,Nginx 里要重新配:
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript
application/xml text/xml image/svg+xml;
gzip_vary on;
location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}检查五:把原来 Apache 的访问日志格式对一遍。如果你有日志分析工具(GoAccess、AWStats)或者自建的统计脚本,Nginx 的 log_format 默认和 Apache 的 combined 不一样,会导致分析工具解析不出来。要么把 Nginx 的格式改成 Apache combined 的样子,要么在分析工具里换解析规则。
要不要迁?先看你的实际情况
说句实话:如果你的网站是低流量的小博客,Apache 完全够用,不必折腾。Apache 的生态成熟、文档多、.htaccess 改起来方便,对新手反而更友好。真正值得迁移的场景是:
· 服务器内存很小(512M~1G),Apache 的进程模型把内存吃光了;
· 站点要承载较高的并发(比如静态资源多、有爬虫压力);
· 你想上 HTTP/2、HTTP/3 或做反向代理/负载均衡,Nginx 的配置更直接;
· 你家站点有多台服务器,需要统一的入口层。
如果决定迁移,牢记一句话:不要为了迁移而迁移,迁移的目标是「所有 URL 行为不变,且资源占用更低」。用上面「URL 清单对比法」做验证,比凭感觉点几下首页可靠得多。迁移过程中最大的风险从来不是 Nginx 配置本身,而是那些隐藏的 .htaccess——尤其是插件、主题、或者你自己半年前随手加的一条规则。
最后提醒一点:迁完之后别忘了把 Apache 的包也卸掉或者至少 disable 掉,否则它会随开机自启占着 80 端口,某次重启之后 Nginx 起不来,你还以为是「服务器抽风」。