Nginx 反向代理:不停服申请 Let’s Encrypt 证书与自动续签
在现有 Nginx 反向代理持续运行的情况下,通过 Certbot webroot 申请免费 HTTPS 证书,配置自动续签和平滑重载,并排查常见验证失败问题。
文章目录
已有服务通过 Nginx 反向代理对外提供 HTTP,想增加 HTTPS,又不能为了申请证书停止服务。本文使用 Certbot webroot 验证 + Nginx 平滑重载:验证文件由 Nginx 直接提供,普通请求继续转发到原来的后端。
用户请求 ──────────────→ Nginx → 现有后端
Let’s Encrypt 验证请求 → Nginx → /var/www/certbot/.well-known/acme-challenge/
Certbot 续签成功 ──────→ 配置检查 → Nginx reload → 新连接使用新证书
这里的“不停服”指流程不要求停止 Nginx,也不重启业务进程。平滑重载会让旧工作进程继续处理已有客户端;错误的代理配置仍可能影响新请求,因此每次修改都要检查并验证业务。参见 Nginx 平滑重载说明。
一、适用环境与示例参数
主流程以 Debian / Ubuntu、systemd 管理的宿主机 Nginx 为例,使用系统软件包安装 Certbot。其他安装方式和 Docker 部署见后文。所有命令中的域名、邮箱、配置文件及后端地址都需要替换。
| 参数 | 本文示例 |
|---|---|
| 域名 | example.com |
| 邮箱 | admin@example.com |
| 后端地址 | http://127.0.0.1:3000 |
| 站点配置 | /etc/nginx/sites-available/example.com |
| 验证根目录 | /var/www/certbot |
| 证书名称 | example.com |
若已有 HTTPS,则在现有配置中更新证书引用;若当前只有 HTTP,则先申请成功,再添加 443 配置。不要提前写入尚不存在的证书文件路径。
二、检查解析、端口和现有服务
- 域名 A 记录应指向实际提供验证文件的入口;有 AAAA 记录时,IPv6 入口也必须正确。
- HTTP-01 验证需要公网能访问该域名的 TCP 80 端口,启用 HTTPS 还需要 443。检查云安全组、主机防火墙和上游端口映射。
- 存在 CDN、WAF 或负载均衡时,验证路径必须能到达正确节点,不能被登录认证、缓存或机器人验证拦截。多节点必须都能返回相同验证文件。
- 确认后端当前正常,并记录一个实际可用的业务 URL,后续用同一请求做前后对照。
sudo nginx -t
sudo systemctl status nginx --no-pager
sudo nginx -T
curl -I http://example.com/
nginx -T 用于确认实际加载的配置文件、server_name、监听端口和代理规则;输出可能包含敏感配置,不要直接公开。若应用不支持 HEAD 请求,用实际的 GET 请求检查。
备份站点配置到 Nginx include 路径之外,避免备份文件也被加载;若修改多个文件,应分别备份,并记下命令输出的备份路径。
sudo install -d -m 700 /root/nginx-backups
sudo cp -av /etc/nginx/sites-available/example.com "/root/nginx-backups/example.com.$(date +%Y%m%d-%H%M%S)"
HTTP-01 不能换成任意公网端口,也不能申请泛域名。无法开放 80 或需要 *.example.com 时,使用后文 DNS-01 方案。参见 Let’s Encrypt 验证方式。
三、安装 Certbot,保留现有安装方式
先检查是否已经安装,以及有没有续签任务:
command -v certbot
certbot --version
systemctl list-timers --all '*certbot*'
若已经通过 Snap、软件包或虚拟环境安装,继续使用原来的方式和可执行文件路径,不要再叠加另一套。尚未安装时,Debian / Ubuntu 可执行:
sudo apt update
sudo apt install certbot
certbot --version
安装前阅读软件包管理器列出的变更;本文不要求升级 Nginx 或整机软件。webroot 模式不需要 Nginx 插件。系统软件包与 Snap 的版本、路径和任务名称可能不同,其他环境按 Certbot 安装文档选择一种方式。
四、给现有 Nginx 增加验证入口
创建专用目录。这里存放公开的验证文件,不存放私钥;目录需允许 Nginx 工作进程读取和遍历。
sudo install -d -m 755 /var/www/certbot/.well-known/acme-challenge
在目标域名已有的 80 端口 server 块中加入下面的 location。保留已有的其他 location、代理请求头和访问规则,不要为同一域名重复新建冲突的 server。
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
auth_basic off;
allow all;
try_files $uri =404;
}
root 会拼接完整请求路径。例如访问 /.well-known/acme-challenge/check,读取的文件是 /var/www/certbot/.well-known/acme-challenge/check。try_files 找不到文件时直接返回 404,不把验证请求交给后端。
下面是原本只有简单 HTTP 代理时的合并示意。已有服务请只合并验证入口,不要用示例覆盖复杂的业务配置:
server {
listen 80;
server_name example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
auth_basic off;
allow all;
try_files $uri =404;
}
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
如果域名通过 IPv6 提供服务,在对应 server 中保留或配置 listen [::]:80;。已有 auth_request、WAF 或额外访问控制时,需要针对这一验证路径单独检查,auth_basic off 不会关闭所有类型的认证。
已存在 HTTP 到 HTTPS 跳转时
若 return 301 或 return 308 写在 server 层,它会先于 location 选择执行。应将普通请求的跳转放进 location /,让验证路径直接返回文件。首次开通 HTTPS 时,要等第七节验证成功后再启用跳转。参见 Nginx rewrite 执行顺序。
# 仅在 HTTPS 已正常工作时,用这段替换 HTTP 的业务 location /
location / {
return 308 https://example.com$request_uri;
}
改完执行配置检查和平滑重载。检查失败就修正配置,不执行重启:
sudo nginx -t && sudo systemctl reload nginx
五、先从公网验证文件路径
printf 'acme-check-ok\n' | sudo tee /var/www/certbot/.well-known/acme-challenge/check > /dev/null
sudo chmod 644 /var/www/certbot/.well-known/acme-challenge/check
在另一台能访问公网的设备执行下面的请求,不加 -L,以便直接看出是否发生跳转:
curl -i http://example.com/.well-known/acme-challenge/check
应返回 HTTP 200,正文为 acme-check-ok。如果返回业务 HTML、认证页面或 404,先修复路由和权限;如果启用了 AAAA,再用 curl -6 验证 IPv6。然后重新请求原来的业务 URL,确认代理仍正常。
验证通过后可以删除测试文件,保留目录和 Nginx location,自动续签仍然需要它们。
sudo rm /var/www/certbot/.well-known/acme-challenge/check
六、申请免费证书
申请前检查现有 Certbot 配置和 /etc/letsencrypt/renewal-hooks/ 下的脚本,确认没有停止服务的 pre/post hook;旧部署留下的 hook 也可能被调用。然后以固定的证书名称申请一个域名:
sudo certbot certonly --webroot \
-w /var/www/certbot \
--cert-name example.com \
-d example.com \
--email admin@example.com \
--agree-tos \
--non-interactive
certonly --webroot 将验证文件写入指定目录,申请证书但不自动安装到 Nginx。此流程不需要让 Certbot 占用 80 端口。若已有同名证书,先检查其域名集合和用途,避免误改其他站点共用的证书。参见 Certbot webroot 文档。
需要同时包含 www.example.com 时,先确认它也能通过第五节测试,再增加 -d www.example.com。不要添加尚未配置的域名。申请失败时先根据日志排错,不要循环强制申请。
sudo certbot certificates
记录输出的 Certificate Name、Certificate Path 和 Private Key Path。下面假设路径为:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
实际名称可能带 -0001 等后缀,以命令输出为准。Nginx 直接引用 live 目录,不复制一份固定证书到其他目录,也不要将私钥放进网站目录。
七、配置 HTTPS 反向代理
证书已经存在后,为目标域名增加 443 server,或修改已有 HTTPS server 的证书引用。下面是简单 HTTP 后端的示意配置:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
有 IPv6 入口时配置相应的 listen [::]:443 ssl;。保留现有 WebSocket、SSE、上传大小、超时、缓存和各业务路径规则。proxy_pass 的尾部斜杠会影响 URI 映射,不要顺手改动;后端也需要正确识别受信代理传入的协议头。参见 Nginx proxy_pass 和 证书配置说明。
sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com/
用实际业务请求检查登录、API、上传及长连接。确认 HTTPS 可用且客户端支持跳转后,才按第四节将 HTTP 普通请求切到 HTTPS;有旧 HTTP API 客户端时,不能仅为申请证书就改变它们的入口行为。验证 location 始终保留。
八、续签成功后自动重载 Nginx
自动化要同时完成“更新证书文件”和“让 Nginx 读取新文件”。这里将成功后的操作写成 deploy hook。先确认命令路径;下面脚本使用 Debian / Ubuntu 常见的 /usr/sbin/nginx 和 /usr/bin/systemctl,其他环境按实际输出替换。
command -v nginx
command -v systemctl
sudo install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx
EOF
sudo chown root:root /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod 750 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
脚本需要由 root 持有且不可被普通用户修改。目录中的可执行 deploy hook 会在成功签发或续签时按当前 Certbot 版本规则调用;它可能服务于多张证书。先检查现有 hook,已有同等功能时合并,避免重复重载。不要配置停止 Nginx 的 pre hook。参见 Certbot 续签与 hook。
先单独运行一次,验证路径和权限:
sudo /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
九、启用并检查自动续签任务
本节对应前面通过 apt 安装的 Certbot。先查看软件包自带任务,再启用;不要直接叠加另一套 cron。Debian 的 Certbot 软件包提供 certbot.service 和 certbot.timer,可查阅 软件包文件列表。
systemctl cat certbot.timer certbot.service
sudo systemctl enable --now certbot.timer
systemctl list-timers --all '*certbot*'
systemctl status certbot.timer --no-pager
确认 timer 已启用,并且列出了下一次运行时间;service 应执行 certbot renew。续签是定期检查,不等于每次运行都重新签发。不要硬编码“每隔多少天强制申请”,也不要把 --force-renewal 放进日常定时任务。
如果安装方式为 Snap,使用它实际提供的续签任务,名称可能类似 snap.certbot.renew.timer;不要在未安装该 unit 的机器上照抄 certbot.timer。没有 systemd 的环境再使用安装方式对应的 cron,命令使用 Certbot 的实际绝对路径。
十、测试续签和线上证书
只测试本文这张证书,避免同时触发现有其他证书的续签流程。证书名称替换为第六节查询到的实际值:
sudo certbot renew --cert-name example.com --dry-run
sudo certbot renew --cert-name example.com --dry-run --run-deploy-hooks
普通 dry-run 不默认执行 deploy hook;第二条会在模拟成功后执行 hook,使用当前有效证书而非临时测试证书。它会真实触发一次 Nginx reload。检查已有 pre/post hook 后再执行;若旧版本不支持该选项,分别运行 dry-run 和第八节的重载脚本。参见 Certbot 测试参数。
sudo journalctl -u certbot.service --since "7 days ago" --no-pager
sudo tail -n 100 /var/log/letsencrypt/letsencrypt.log
sudo certbot certificates
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
最后一条读取线上实际返回的证书。dry-run 本身不会延长正式证书有效期;正常续签后,应再次核对线上到期时间。若域名前有 CDN,这里看到的是 CDN 边缘证书,还需使用源站地址连接并携带域名 SNI,分别核对两层 TLS。
将续签失败、重载失败和线上证书接近到期纳入监控。单看 timer 正常或文件已更新,不能证明 Nginx 已使用新证书。
十一、常见故障排查
| 现象 | 优先检查 |
|---|---|
| 验证 404 或返回业务 HTML | server_name 是否命中;root 与 -w 是否一致;try_files 是否回退到后端;验证文件是否位于正确目录。 |
| 验证 403 或登录页面 | 目录读取/遍历权限、隐藏目录拦截、认证、WAF;SELinux / AppArmor 环境还需检查相应访问策略。 |
| 超时或部分请求失败 | 公网 80、安全组、防火墙、A/AAAA 解析、负载均衡节点及 CDN 路由。 |
| 验证请求被跳转 | server 层 return/rewrite 是否抢先执行;跳转后是否进入登录页或错误站点。 |
| 签发被拒绝 | 阅读 Certbot 日志中的具体原因;检查域名 CAA 限制和签发限额,修正后再重试。 |
| HTTPS 502 | 证书与后端代理是不同环节;检查后端存活、地址、端口和 proxy_pass 路径。 |
| 文件更新但线上还是旧证书 | deploy hook 是否执行、nginx -t 是否失败、是否加载了另一张证书或访问了其他节点/CDN。 |
| timer 不存在 | 检查安装来源和 systemctl list-timers 输出,使用实际安装方式提供的任务。 |
如果本次修改影响业务,将本次变更的文件恢复到第二节的备份版本;新建的配置需同时移除其 include 或启用链接,然后先执行 nginx -t,通过后再 reload。不要通过删除证书文件来回滚,也不要直接重启仍能处理原有连接的服务。
十二、DNS-01 与 Docker 部署差异
无法开放 80,或需要泛域名
改用 DNS-01:由 DNS 插件调用服务商 API 写入 _acme-challenge TXT 记录。若希望无人值守续签,需要支持自动更新记录的 API 或自动化 hook;手动粘贴 TXT 记录的流程不能直接变成自动续签。凭据只授予目标域名所需权限,并按插件文档限制文件访问。参见 DNS-01 说明。
Nginx 运行在 Docker 中
- Certbot 写入的验证目录必须与 Nginx 容器读取的目录对应。可以由宿主机 Certbot 写入 /var/www/certbot,再把该目录只读挂载给 Nginx。
- 证书目录应挂载完整的 /etc/letsencrypt,而不只挂载 live 子目录,因为 live 中的文件通常通过符号链接指向 archive。Nginx 只需要读取权限。
- 重载 hook 应在正确的容器内依次运行 nginx -t 和 nginx -s reload;宿主机的 systemctl reload nginx 不会重载容器中的 Nginx。
- 如果现有容器尚未挂载验证或证书目录,新增挂载通常需要重建容器,这一步可能中断服务。严格不停服时应预先规划共享目录或滚动切换,不能直接套用本文宿主机命令。
十三、完成检查
- 现有业务在修改前后都能正常访问,后端没有因证书操作被停止。
- 目标域名通过 HTTP 可以读取验证文件,普通请求仍按预期代理或跳转。
- HTTPS 返回正确域名的证书,实际业务接口、登录和长连接正常。
- Nginx 直接引用正确的 live 证书路径,验证入口长期保留。
- 自动续签任务已启用,有下一次执行时间,没有重复调度。
- 模拟续签和重载脚本均验证成功,日志与线上到期监控可用。
整个流程的关键是让域名验证走独立的静态路径,让证书更新通过平滑重载生效。以后只要验证入口、续签任务和重载脚本持续可用,就无需为了每次续签手动停服。