Nginx 反向代理 + Let's Encrypt 证书自动续期实战

后端服务直接暴露在公网有两件事很难办:套 HTTPS 麻烦、被扫描器盯上。标准做法是在前面架一层 Nginx 做反向代理——证书、压缩、限流、日志都在这一层收口。我这台 2C4G 的小机器上目前并排跑着 7 个后端服务(图床、面板、API 网关……),对外只开 80/443 两个口,其余全部只监听 127.0.0.1。

结论先给:证书这件事全站只维护一次,加新服务只需要复制一段 location。

为什么不给后端直接上证书

维度 后端自己签证书 Nginx 统一收口
证书维护 每个服务一份,续期要逐个改 全站一份,一次配好
端口 每个服务都要对外开一个口 只开 80/443
压缩 / 缓存 / 限流 各框架各写一遍 配置层统一处理
换后端 改证书绑定 只改 proxy_pass 一行
攻击面 N 个服务暴露 N 个端口 只暴露 Nginx

反代不是"多一层",而是把通用能力从业务里剥出来放进网关层。下面这套配置我用了大半年,加服务就是复制粘贴。

请求路径

Nginx 反向代理的请求路径

用户 → Nginx(443,持有证书)→ 后端服务(只监听 127.0.0.1)。后端完全不接触公网,证书也只有 Nginx 一处要管。

操作步骤速览

反代 + 证书配置步骤

一、基础反代配置

步骤 1:配置反向代理

/etc/nginx/conf.d/app.conf:

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    # 安全响应头(先加这几条,再谈别的优化)
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;

    client_max_body_size 100m;          # 默认 1m,传大文件会 413

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
    gzip_min_length 1024;

    access_log /var/log/nginx/app.access.log;
    error_log  /var/log/nginx/app.error.log warn;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 10s;
        proxy_read_timeout    60s;
        proxy_send_timeout    60s;
    }
}

X-Forwarded-* 这几行不是可选项——不传给后端,后端日志里看到的全是 127.0.0.1,出问题根本没法查。

改完先让 Nginx 自检,再平滑加载(永远别用 restart 赌配置):

nginx -t && systemctl reload nginx

二、签证书并自动续期

步骤 2:签发证书

apt install -y certbot python3-certbot-nginx
certbot --nginx -d app.example.com

--nginx 插件会自动改写你的 server 块,填好 ssl_certificate 那段。它同时会注册一个 systemd 定时器,这才是"自动续期"的真身:

步骤 3:确认自动续期

systemctl list-timers | grep certbot
# certbot.timer  → 下次触发时间在 30 天内

certbot renew --dry-run      # 演练一次,不真的续

看到 Congratulations, all simulated renewals succeeded 就说明自动续期没问题。

但这里有个官方文档容易漏掉的点:续期成功后,Nginx 未必自己去读新证书。要在 /etc/letsencrypt/renewal-hooks/deploy/ 里放一个重载脚本:

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
nginx -t && systemctl reload nginx
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

我一开始以为装了 certbot 就万事大吉,直到某天浏览器报"证书还有 0 天到期"才反应过来——续期了,但进程还捏着旧证书。加了 deploy hook 之后半年没再管过。

三、四个最常见的坑

步骤 4:修掉四个最常见的坑

① WebSocket 连不上。 后端有实时推送(面板、终端、IM)时会卡在握手阶段,在同一 location 里补两行:

proxy_set_header Upgrade    $http_upgrade;
proxy_set_header Connection "upgrade";

② 上传大文件返回 413。 Nginx 默认限制请求体 1MB,在 server 块加 client_max_body_size 100m;(上面已含)。

③ 后端拿到的全是 127.0.0.1。 漏了 X-Forwarded-For;注意后端框架也要显式开启"信任代理头",否则真实 IP 一样取不到。

④ 站点套了 Cloudflare 橙云后,certbot 续期失败。 HTTP-01 挑战会被 CF 挡在自己那边。两种解法:签发时切灰云(DNS only)跑一次,或改用 DNS 挑战:

certbot --nginx -d app.example.com --preferred-challenges dns

这四个坑几乎每个人都要踩至少一个,先记下来能省半天。

四、进阶:多后端与限流

服务一多,同一套配置就该升级成"网关"。两件事最先需要:同一域名的多后端分流、挡住异常高频请求。

# http {} 段:定义限流区与后端池
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;

upstream app_pool {
    least_conn;                       # 谁闲派给谁
    server 127.0.0.1:8080 weight=3;   # 主节点,权重高
    server 127.0.0.1:8081;            # 备用节点
    keepalive 32;                     # 长连接复用,省握手
}

server {
    # ...
    location /api/ {
        limit_req zone=api burst=40 nodelay;   # 突发放行 40 个,超出直接 503
        proxy_pass http://app_pool;
        proxy_set_header Connection "";
    }
}
  • least_conn:按当前连接数派发,比轮询更贴合真实负载;
  • keepalive 32:Nginx 与后端之间复用长连接,高并发下能省掉大量 TCP 握手;
  • limit_req:给接口类路径按 IP 限速,脚本刮站、异常重试都会被削平,burst 是允许的瞬时突发量。

限流阈值怎么定:先看正常流量的峰值(access.log 里按分钟统计),把 rate 设在峰值的 2–3 倍,burst 设为峰值的 10 倍左右。定太紧会误伤真实用户,定太松等于没限。

效果实测

同一台机器、同一个后端(Node 服务),直连后端端口 vs 走 Nginx 反代的 TTFB(curl 本地测 20 次的均值):

路径 TTFB 说明
直连 127.0.0.1:8080 42 ms 基线
经 Nginx 反代 45 ms 多一跳,+3 ms 可忽略
反代 + gzip(首屏 HTML 38 KB → 9 KB) — 传输量降约 76%

结论很直白:反代的性能代价接近零,换来的是全站证书、压缩、限流的统一治理。

FAQ

Q:证书续期还需要人工盯着吗?
不需要。certbot.timer 到期前会自动续,配好 deploy hook 后 Nginx 会自己重载。留个体检习惯即可:certbot certificates 随时看剩余天数。

Q:反代之后怎么让后端识别访客的真实来源?
透传 X-Forwarded-For / X-Real-IP / X-Forwarded-Proto 三个头,并在后端显式信任代理。别只做一半——只传不信,后端仍然会把代理 IP 当访客。

Q:同一台机器上 8 个服务要写 8 份配置吗?
不用。每个服务一个 conf.d/*.conf,只改 server_name 和 proxy_pass 两处,公共部分(安全头、gzip、日志格式)放到 nginx.conf 的 http {} 里统一继承。

Q:证书到期前怎么提前知道?
两条路:一是让它自己续(你只要保证 certbot renew --dry-run 能过);二是加监控,certbot certificates 输出的剩余天数可以接进 Uptime-Kuma 的"证书到期"探针。到期当天才发现是最糟糕的体验,提前一周就有告警才安心。

Q:反代会不会让我的站点变慢?
不会。多一跳的开销通常在 3–5 ms,而 gzip 与长连接复用省下的传输时间远超这点延迟。实测见上面的表格。

Q:能不能把 HTTP/2 换成 HTTP/3(QUIC)?
可以。Nginx 1.25+ 支持 listen 443 quic reuseport;,再加 add_header Alt-Svc 'h3=":443"'。但要先确认防火墙放行了 UDP 443,否则浏览器会回落到 HTTP/2,白折腾一场。

发布前自查

  • [ ] nginx -t 通过后才 reload
  • [ ] 80 端口已 301 跳到 https
  • [ ] X-Forwarded-For 三件套已透传且后端已信任
  • [ ] client_max_body_size 够大,WebSocket 头已补
  • [ ] certbot renew --dry-run 成功,deploy hook 已就位
  • [ ] 把这份配置备份进 git 或别的机器——重建时能省一整天

配置这东西,"配好"和"记录好"是两件事。把 conf.d/ 整个目录纳入版本管理,下次换机器就是一条 git clone 的事。反代这种基础层配置,值得多花五分钟归档。

小结

反代 + Certbot 这套组合,本质是把「证书管理」和「服务暴露」两件事从后端剥离出来。配一次,后面加新服务只需要复制一段 location。三分钟配好,管三年。

常见问题

证书续期需要手动操作吗?

不需要。安装 Certbot 时会注册 systemd 定时器,到期前自动续期;随时可用 certbot renew --dry-run 演练确认。真正要自己补的是「续期后通知 Nginx 重载」这一步,见正文 deploy hook。

反代后后端拿到的真实 IP 都是 127.0.0.1 怎么办?

漏了 X-Forwarded-For 头。在 location 里加上 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for,并在后端信任该头即可。

相关阅读

上一篇 Codex CLI 接入第三方模型:config.toml 里的 wire_api 是个坑(2026)
下一篇 本地跑大模型:Ollama + Open WebUI 一条龙部署