后端服务直接暴露在公网有两件事很难办:套 HTTPS 麻烦、被扫描器盯上。标准做法是在前面架一层 Nginx 做反向代理——证书、压缩、限流、日志都在这一层收口。我这台 2C4G 的小机器上目前并排跑着 7 个后端服务(图床、面板、API 网关……),对外只开 80/443 两个口,其余全部只监听 127.0.0.1。
结论先给:证书这件事全站只维护一次,加新服务只需要复制一段 location。
为什么不给后端直接上证书
| 维度 | 后端自己签证书 | Nginx 统一收口 |
|---|---|---|
| 证书维护 | 每个服务一份,续期要逐个改 | 全站一份,一次配好 |
| 端口 | 每个服务都要对外开一个口 | 只开 80/443 |
| 压缩 / 缓存 / 限流 | 各框架各写一遍 | 配置层统一处理 |
| 换后端 | 改证书绑定 | 只改 proxy_pass 一行 |
| 攻击面 | N 个服务暴露 N 个端口 | 只暴露 Nginx |
反代不是"多一层",而是把通用能力从业务里剥出来放进网关层。下面这套配置我用了大半年,加服务就是复制粘贴。
请求路径

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

一、基础反代配置

/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
二、签证书并自动续期

apt install -y certbot python3-certbot-nginx
certbot --nginx -d app.example.com
--nginx 插件会自动改写你的 server 块,填好 ssl_certificate 那段。它同时会注册一个 systemd 定时器,这才是"自动续期"的真身:

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 之后半年没再管过。
三、四个最常见的坑

① 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,并在后端信任该头即可。