背景
服务器是阿里云轻量应用服务器,上面跑着 Halo 博客,域名正在备案。备案没过之前 443 开不了,证书也签不了,所以 HTTPS 这件事一直被推到最后。
推到最后的结果是:方案早就写好了,真到要跑的时候才发现它跑不了。
这篇文章记的是"一个全新 Halo 站 + 一份网上抄来的 Nginx 反代脚本",会遇到哪些互相打架的地方。
坑 1:脚本要 apt install nginx,可 nginx 早就装好了
最常见的一类错误是:脚本假设服务器是台"什么都没有"的干净机器,于是上来就装一遍该装的东西。
但实际情况是,你的 Halo 前面早就站着一个 Nginx 了。你甚至可能就是靠它才能访问到 Halo。
这时候再执行一次:
apt install -y nginx
apt 会发现"系统里已经有个 nginx 了",然后把你那个 1.27.2 降级成发行版仓库里的旧版本。降级过程中配置文件格式不兼容、服务重启失败,站点就没了。
教训:写运维脚本的第一步应该是检测现状,而不是假设现状。 判断逻辑应该是"nginx 在吗?在就跳过装,只校验版本和配置"。
坑 2:limit_req_zone 写在了 server 块里
限流是好事,但指令放错层级会直接让 Nginx 拒绝启动:
server {
listen 80;
limit_req_zone $binary_remote_addr zone=halo_limit:10m rate=10r/s; # ❌ 语法错误
}
limit_req_zone 只允许出现在 http 上下文(也就是配置文件的最外层,或者 include 进来的文件顶层),写在 server 甚至 location 里面,nginx -t 会直接报错:
nginx: [emerg] "limit_req_zone" directive is not allowed here
而 include /etc/nginx/conf.d/*.conf; 这行本身就在 http 块里,所以把配置文件写成独立文件丢进 conf.d,天然就是正确的上下文。
# /etc/nginx/conf.d/halo-site.conf —— 顶层,直接写
limit_req_zone $binary_remote_addr zone=halo_limit:10m rate=30r/s;
坑 3:add_header 在 location 里会把安全头全部吃掉
这是 Nginx 里最隐蔽的一个继承规则。
安全响应头通常写在 server 块:
server {
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
}
但只要任何一个 location 块里也写了 add_header,那么这个 location 里的响应就只会带上 location 自己写的那几条,server 级的全部作废:
location ~* \.(js|css|png)$ {
proxy_pass http://127.0.0.1:8090;
add_header Cache-Control "public, immutable"; # ❌ 安全头在这里被清空了
}
规则不是"合并",是"覆盖"。所以静态资源虽然不会被告警,但也拿不到 X-Frame-Options 了。
正确做法:安全头全部放 server 级,location 里改用 expires:
location ~* \.(js|css|png)$ {
proxy_pass http://127.0.0.1:8090;
expires 30d; # ✅ 走 expires 指令,不影响 add_header 继承
}
坑 4:签证书之前就把 HTTP 跳成了 HTTPS
很多人的脚本里,80 端口的跳转是一开始就打开的:
location / {
return 301 https://$host$request_uri;
}
问题是:证书是下一步才签的。签证书需要通过 80 端口访问 /.well-known/acme-challenge/ 来验证域名归属。
如果 80 端口一上来就 301 到 https,而此时 443 上还没有证书、连 443 server 块都还没写——整个站点在这几十秒里是死的。
正确顺序是分两阶段:
- 阶段一:80 端口正常反代 Halo,ACME 路径单独放行 → 签证书
- 阶段二:证书拿到后,把 80 改成 301,加 443 server 块 → 切 HTTPS
切换过程中站点不中断。
坑 5:Connection "upgrade" 被无条件下发
WebSocket 代理的经典写法是:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
问题在于第二行是写死的。哪怕这个请求压根不是 WebSocket 升级(普通页面加载、API 调用),也照样告诉后端"我要升级协议"。
规范做法是用一个 map 控制:
# http 上下文
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; # ✅ 有才升级,没有就 close
}
坑 6:写完配置不检查"到底生效没有"
最隐蔽的一种失败:你把配置文件写到了某个目录,nginx -t 也过了,脚本打印"✅ 配置已生效"——但那个目录根本没被 nginx.conf include。
配置文件放错地方 = 什么都没发生,脚本却报成功。
两道保险:
# 1. 确认 include 路径
grep -n "include" /etc/nginx/nginx.conf
# 2. 配置写完后,用 -T 导出"实际生效"的完整配置再查一遍
nginx -T | grep -q "server_name example.com" || { echo "配置没被载入"; exit 1; }
nginx -T 和 nginx -t 的区别是:-t 只做语法检查,-T 会把真正加载进内存的完整配置打印出来。用它来做"生效校验"最可靠。
附带一条:别把 Halo 端口暴露到公网
最后说个做对了的。
很多人改完 Nginx 会顺手开一个 8090 端口让 Halo 直连,方便调试。这样就等于绕过了 Nginx——前面配的所有限流、安全头、以后加的 HTTPS 全部失效。
正确做法是让 Halo 只监听 127.0.0.1,公网只暴露 80/443:
# 确认外网访问不到 Halo 自身端口
curl -s -o /dev/null -w "%{http_code}" http://<你的IP>:8090
# 期望:000(连接被拒)
小结
这六条里,没有一条是"知识盲区",全都是假设现状和没验证结果造成的:
| 坑 | 本质 |
|---|---|
| 重装 nginx | 假设了服务器状态 |
| limit_req_zone 层级 | 没查指令的上下文要求 |
| add_header 覆盖 | 不清楚继承规则 |
| 提前 301 | 没考虑中间态 |
| Connection 写死 | 抄了习惯写法 |
| 不校验生效 | 只做了语法检查,没做结果检查 |
运维脚本里,"我觉得配好了"和"它真的生效了"之间,差的往往是最后那两行校验。
备案还有几天要等,正好有时间把这套东西理顺。等域名一解析过来,直接跑就行。