KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
06 · nginx HTTPS 部署:配置、兼容坑与回滚 — keel 龙骨
本章目标:HTTPS 返回 200,证书链完整,HTTP 全部 301 到 HTTPS。 验收标准:curl -I https://www.xxxxxx.cn/ 返回 200,且 curl -I http://www.xxxxxx.cn/ 返回 301。
本章目标:HTTPS 返回 200,证书链完整,HTTP 全部 301 到 HTTPS。
验收标准:curl -I https://www.xxxxxx.cn/返回 200,且curl -I http://www.xxxxxx.cn/返回 301。
一、完整配置骨架(可直接抄)
设计原则:两个 server 块,80 只做跳转,443 承载全部业务 location。 不要试图在一个块里用 if ($scheme = http) 判断,也不要把 location 复制两份。
# ---------- 80:全站 301 到 https ----------
server {
listen 80;
server_name www.xxxxxx.cn xxxxxx.cn;
# 证书校验通道必须留在 http 侧,不能被下面的 301 吃掉
location /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
# ---------- 443:业务全部在这里 ----------
server {
listen 443 ssl http2; # ← 注意写法,见第二节
server_name www.xxxxxx.cn xxxxxx.cn;
ssl_certificate /etc/nginx/ssl/xxxxxx.cn_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/xxxxxx.cn.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# 反向代理时把协议告诉后端,否则后端生成的链接还是 http
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
root /home/ubuntu/fqm/projects/learning-platform/public;
index index.html;
location /assets/ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location /learning/api/ {
proxy_pass http://127.0.0.1:5177; # 后端服务保持不变
}
location / {
try_files $uri $uri/ /index.html;
}
access_log /var/log/nginx/learning.access.log;
error_log /var/log/nginx/learning.error.log;
}
二、版本兼容坑:http2 on; 会让 nginx -t 直接失败
这是本章最实际的一个坑。 nginx 1.25.1 之前不支持独立的 http2 on; 指令,必须写成:
listen 443 ssl http2; # ✅ nginx < 1.25.1 用这个
而新版本推荐(未来会强制):
listen 443 ssl;
http2 on; # ✅ nginx >= 1.25.1
先查版本再决定:
nginx -v
写错的表现是 nginx -t 报 unknown directive "http2",因为做了备份所以会自动回滚——但这正是第 04 章强调"先备份再改"的原因:没有备份的话,这个错误会让 nginx 停在损坏的配置上,站点直接下线。
另外注意 ssl on; 这个古老写法同样已被废弃(1.15+ 会告警,之后移除)。统一用 listen ... ssl。
三、$host 还是 $server_name?
return 301 https://$host$request_uri;
用 $host:它取的是请求里的 Host 头,能正确处理 xxxxxx.cn → https://xxxxxx.cn/ 的情况。
用 $server_name 会把所有域名都跳到 server 块里的第一个名字,导致裸域名的访问被硬拉到 www 上——如果你希望保留两个域名各自跳转(而非统一到 www),这是错的。
若你决定统一到 www(推荐,配合 canonical):
server {
listen 80;
server_name xxxxxx.cn www.xxxxxx.cn;
return 301 https://www.xxxxxx.cn$request_uri;
}
server {
listen 443 ssl http2;
server_name xxxxxx.cn;
ssl_certificate /etc/nginx/ssl/xxxxxx.cn_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/xxxxxx.cn.key;
return 301 https://www.xxxxxx.cn$request_uri;
}
三个 server 块:80 全跳 www、443 裸域名跳 www、443 www 承载业务。HTTPS 跳转必须发生在 TLS 握手之后,所以裸域名的 443 块也要配证书,否则用户访问 https://xxxxxx.cn 会先看到证书告警。
四、上线动作序列(顺序不能乱)
# 1. 备份现网配置
sudo cp /etc/nginx/sites-enabled/learning /tmp/learning.http.bak
# 2. 写入新配置(用编辑器或 cat > 重定向)
sudo vim /etc/nginx/sites-available/learning
# 3. 语法检查 —— 失败就到此为止
sudo nginx -t
# 4. reload(不是 restart:不断连接)
sudo systemctl reload nginx
# 5. 验收
curl -I https://www.xxxxxx.cn/
curl -I http://www.xxxxxx.cn/
reload 与 restart 的区别要记住:reload 会等旧 worker 处理完现有连接再退出,不掉请求;restart 会短暂中断。生产环境一律 reload。
回滚(30 秒内可完成)
sudo cp /tmp/learning.http.bak /etc/nginx/sites-enabled/learning
sudo nginx -t && sudo systemctl reload nginx
把备份和回滚命令放在同一段脚本里,失败时不用现想:
CONF=/etc/nginx/sites-enabled/learning
sudo cp $CONF /tmp/learning.bak.$(date +%s)
sudo nginx -t || { sudo cp /tmp/learning.bak.* $CONF; sudo systemctl reload nginx; exit 1; }
sudo systemctl reload nginx
五、验收:不只是"能打开"
# 1. HTTP → HTTPS 是否 301
curl -sI http://www.xxxxxx.cn/ | grep -i '^location'
# 期望:location: https://www.xxxxxx.cn/
# 2. 证书链是否完整(看输出里的证书数量与 Verify return code)
echo | openssl s_client -connect www.xxxxxx.cn:443 -servername www.xxxxxx.cn 2>/dev/null \
| openssl x509 -noout -dates -issuer
curl -vI https://www.xxxxxx.cn/ 2>&1 | grep -i 'SSL certificate verify'
# 3. 协议与 HTTP/2 是否生效
curl -sI --http2 https://www.xxxxxx.cn/ | head -1
# 4. 重定向链有没有多跳(只应有 1 跳)
curl -sIL http://xxxxxx.cn/ | grep -iE '^(HTTP/|location)'
第 2 项最容易出问题:如果 openssl s_client 报 Verify return code: 20 (unable to get local issuer certificate),说明你用的是单张域名证书而非 bundle,缺了中间证书。回到第 05 章,下载时选带 bundle 的那个文件。
第 4 项:重定向链每多一跳,爬虫和用户都要多付一次 RTT。理想状态是 http://xxxxxx.cn → https://www.xxxxxx.cn 一次到位(上面统一到 www 的配置可以做到,如果把 80 先跳 https 裸域名再跳 www,就变成两跳了)。
六、可选加固:HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
作用:告诉浏览器"以后这个域名只许用 https 访问",省掉一次跳转并防降级攻击。
⚠️ 加之前想清楚:一旦浏览器记住了 HSTS,在 max-age 内你的 https 出问题,用户无法通过访问 http 绕过,也无法点击"继续访问不安全网站"。
建议先设短一点(max-age=300)验证一周,确认 HTTPS 稳定后再改成一年。includeSubDomains 会影响所有子域,有非 HTTPS 子域时别加。
七、本章验收清单
-
nginx -t通过(已确认http2写法与自己的 nginx 版本匹配) -
https://www.xxxxxx.cn/返回 200,证书链完整无告警 -
http://全站 301 到https://,重定向只有 1 跳 - 裸域名与 www 都能正常到达目标站点(无证书告警)
-
/.well-known/acme-challenge/未被 301(为下次续期留门) - 配置已备份,回滚命令已验证可用
HTTPS 通了只是表象——下一步要检查页面内部是否还残留 http,那才是"能打开但有小毛病"的根源。