← 全部文章

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/checktry_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 301return 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.servicecertbot.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 或返回业务 HTMLserver_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 证书路径,验证入口长期保留。
  • 自动续签任务已启用,有下一次执行时间,没有重复调度。
  • 模拟续签和重载脚本均验证成功,日志与线上到期监控可用。

整个流程的关键是让域名验证走独立的静态路径,让证书更新通过平滑重载生效。以后只要验证入口、续签任务和重载脚本持续可用,就无需为了每次续签手动停服。

搜索文章

输入关键词,搜索所有文章。