首页 / 资讯中心 / 文章详情

Gunicorn直连HTTPS证书自动续期:从原理到热重载完整方案

Gunicorn直连HTTPS证书自动续期:从原理到热重载完整方案 ★ FEATURED ARTICLE
在 Gunicorn 里做 HTTPS本身就有那么点“偏要和别人不一样”的味道——绝大多数团队都会老老实实把 Nginx 或 Caddy 放到前面终结 TLSGunicorn 只负责跑 WSGI 应用。但你一旦决定让 Gunicorn 直接面向客户端暴露 HTTPS或者你所在的部署环境压根就没有反向代理这一层那么“Let‘s Encrypt 证书自动更新”这件事就会从“顺手配置一下”变成“必须设计一套机制”。这篇东西就是写给那些和我一样被“Gunicorn 直连 证书自动续期”折磨过或者正准备这么干的同学。我会把为什么 Gunicorn 下做证书续期比 Nginx 下要绕、以及怎么绕过去全部拆开讲清楚。先说结论Gunicorn 本身只是一个 WSGI 容器它不像 Nginx 那样会自动帮你重读磁盘上的证书文件。你就算把--certfile和--keyfile配好了Let‘s Encrypt 的 certbot 每 90 天默默帮你把证书续掉Gunicorn 也浑然不知它会一直抱着旧证书跑到天荒地老直到客户端报错、你手动重启进程为止。所以“配置证书自动更新”的真正重点不在于 certbot 怎么装、怎么配而在于“Gunicorn 怎么在新证书落地之后优雅地把新证书加载进内存”。本篇会围绕这个核心坑展开覆盖从证书申请、Gunicorn 配置、热重载机制到定时续期任务的全链路方案。1. 架构选型为什么非要在 Gunicorn 这一层解决证书问题1.1 三种常见部署形态的取舍我见过很多人在这一步就已经绕晕了。Gunicorn 官方文档里其实不太鼓励你直接拿它做 HTTPS 服务。它给出的建议是用 Nginx 反代终结 TLSGunicorn 只监听 127.0.0.1 的内部端口。这套方案的好处是证书续期根本不用动 Gunicorn——Nginx 会自动重读证书文件你甚至可以用nginx -s reload实现秒级热切换。但现实里总有三类场景会让你绕不开“Gunicorn 直连”这条路第一种纯内网工具系统。公司内部某个管理后台、某个数据处理平台用户量就是几十人架构师或者老大明确说“别搞那么多层能跑就行”。这时候 Nginx 就是多余的中间层Gunicorn 直监听 443 端口的成本最低。第二种云原生环境里的轻量部署。容器平台本身已经做了流量入口的负载均衡和证书管理但某些独立 Pod 或者边缘服务需要自己暴露 HTTPS又不想为了这个单独引入 Nginx 容器。Gunicorn 直接配 TLS 就成了最务实的选项。第三种写 Python 的服务端开发团队团队里缺少专职运维。让开发去维护一套 Nginx 配置 upstream 静态文件规则出错概率远高于让 Gunicorn 自己多两个参数。我的一个朋友在某创业公司就是这么干的一行 Nginx 都没写Gunicorn 把证书一挂站点就绿了。不过有一点必须说清楚Gunicorn 的并发模型是基于 worker 进程的TLS 握手在 Python 进程内完成性能比起 Nginx 的 OpenSSL 异步处理确实有差距。如果你的流量已经大到需要纠结 QPS那就不该看这篇文章老老实实加一层 Nginx。Gunicorn 直连适合的场景是中小流量、低并发、追求架构简单和部署可控的服务。1.2 Gunicorn 直连模式下证书续期麻烦在哪先说清楚 Gunicorn 处理证书的方式。Gunicorn 接收--certfile和--keyfile参数后会在 worker 进程初始化时读取这两个文件把证书和私钥加载进内存然后建立 SSL context。问题在于这个 SSL context 一旦建立就不会主动去磁盘上重新读取证书文件——它不会像一个文件监视器那样在证书更换后自动 reload。所以你面临的核心矛盾是certbot 定期续期证书并覆盖同名文件但 Gunicorn 的 worker 进程还握着一份旧的 SSL context 不放。客户端连上来后Gunicorn 用的依然是旧证书内容。浏览器会立刻报警告因为证书有效期已过期或指纹不匹配。解决思路不外乎两条路一条是“续期后的新证书文件名或版本信息发生变变化触发 Gunicorn 重新加载”另一条是“干脆重启 Gunicorn 进程让它重新读盘”。这两条路我后面都会详细介绍实现但先记住一个判断标准项目允许秒级服务中断就直接重启不允许中断就做热重载。两者在自动化实现上差别不小。2. Let‘s Encrypt 证书机制自动化的前提是你得先懂它的游戏规则2.1 90 天有效性背后的设计逻辑Let‘s Encrypt 把证书有效期强制设在 90 天而不是像传统付费证书那样一办一年。这个设计很容易被误解成“麻烦”但实际是故意为之——短有效期配合自动化续期才能在密钥泄露时把影响窗口压缩到最小。这套机制也意味着“设置一次、永久自动”是完全可以实现的但前提是你不能指望手工去续。90 天听起来宽裕实际运维中我给一个建议把续期检查频率设为每天两次。哪怕 certbot 默认只会在证书剩余寿命低于 30 天时才执行续期每天跑一次系统定时任务也就占用几秒钟 CPU 和一次 ACME 服务器通信稳得很。2.2 ACME 协议与两种验证模式Let‘s Encrypt 用的是自动证书管理环境协议全称 Automated Certificate Management Environment通常叫 ACME。certbot 作为 ACME 客户端代表你向证书颁发机构证明“你确实控制这个域名”从而获发证书。证明方式最常见的两种一种是standalone模式。certbot 自己起一个临时 HTTP 服务监听 80 端口然后 ACME 服务器通过http://你的域名/.well-known/acme-challenge/xxx这个路径来验证文件确实存在。这种模式的问题是80 端口必须空闲而 Gunicorn 直连模式下通常 80 端口还跑着应用服务冲突是必然的。另一种是webroot模式。certbot 不自己起服务只把验证文件写入你指定的某个 Web 根目录比如/var/www/html然后由你现有的 Web 服务器Nginx / Apache / Gunicorn 应用本身把这个路径暴露出去供 ACME 服务器访问。这才是 Gunicorn 直连模式下最合理的验证方式避开端口冲突。这里有个很多人会忽略的坑Gunicorn 本身根本不托管静态文件如果你希望用 webroot 模式得在 Flask 或 Django 应用里手动加一个路由把/.well-known/acme-challenge/的请求映射到磁盘目录上。比如 Flask 里加一个静态路由指向/var/www/challengesDjango 里配一个 url.py 的映射。否则 ACME 服务器访问验证路径时会拿到 404续期就失败了。2.3 证书文件目录与实际落盘位置certbot 默认把证书放在/etc/letsencrypt/live/你的域名/下面里面有四个关键文件cert.pem只包含服务器证书不包含中间证书。单独用它在某些客户端上可以跑通但为了兼容最全的客户端校验链建议使用fullchain.pem。chain.pem包含中间证书链。fullchain.pemcert.pem 和 chain.pem 的内容拼接。Gunicorn 的--certfile指向这个文件最稳妥。privkey.pem私钥。Gunicorn 的--keyfile指向它。注意/etc/letsencrypt目录的默认权限控制得很严root 用户才能读取privkey.pem。如果 Gunicorn 是以普通用户身份运行的比如www-data或者gunicorn用户你得手动给这个用户授权访问路径或者把证书复制到应用目录并设置好属主和服务用户相匹配。3. Gunicorn 侧负载配置证书并解决热加载问题3.1 基础配置certfile 和 keyfile 的参数详解Gunicorn 启动时直接带参数CD 到项目目录里后用类似这样的命令就能跑起来gunicorn -w 4 -b 0.0.0.0:443 \ --certfile/etc/letsencrypt/live/example.com/fullchain.pem \ --keyfile/etc/letsencrypt/live/example.com/privkey.pem \ app:app如果用的是 systemd 管理Unit 文件里 ExecStart 那行改成ExecStart/usr/local/bin/gunicorn -w 4 -b 0.0.0.0:443 \ --certfile/etc/letsencrypt/live/example.com/fullchain.pem \ --keyfile/etc/letsencrypt/live/example.com/privkey.pem \ app:app配置归配置真正的问题还是那句证书续期之后Gunicorn 会拿新证书吗答案是不会。你改了磁盘上的文件worker 进程内存里的 SSL context 不变。这就是我们要解决的核心问题。3.2 方案一systemd exec restart以秒为单位接受中断最简单粗暴的方式就是在 certbot 续期成功后重启 Gunicorn 服务。对于内部工具、管理后台这类允许短暂断连的项目完全可行。systemd 的 exec restart 类型能保证老进程退出后新进程立刻拉起来整体中断时间通常不超过 1 秒。实现方式写一个 deploy hook 脚本放在/etc/letsencrypt/renewal-hooks/deploy/目录下#!/bin/bash # /etc/letsencrypt/renewal-hooks/deploy/gunicorn-restart.sh systemctl restart gunicorn.service然后授予执行权限chmod x /etc/letsencrypt/renewal-hooks/deploy/gunicorn-restart.sh只需在每次续期完成后certbot 把所有部署钩子依次执行你的 Gunicorn 就会重启并加载新证书。这套配置的优点是简单可靠缺点也很明显——服务有瞬时中断。如果你的业务对连接连续性有硬性要求比如长连接很多、实时任务不能断就得看下一个方案。3.3 方案二信号触发 Gunicorn 热重载零中断换证书Gunicorn 支持通过发送 HUP 信号给主进程来执行平滑重载。这个机制原本是为代码更新设计的代码文件变了之后发个kill -HUP $(cat /var/run/gunicorn.pid)Gunicorn 会重启 worker 进程并重新加载应用。但你会发现新 worker 启动时同样会重新读取--certfile和--keyfile指定路径下的证书文件所以这招对证书更新同样有效。流程拆解确保 Gunicorn 主进程的 PID 写到了固定路径。启动时加--pid/var/run/gunicorn.pid。certbot 续期成功后deploy hook 脚本向这个 PID 发送 HUP 信号。Gunicorn 主进程收到 HUP逐个平滑重启所有 worker新 worker 在 init 阶段重新加载证书。部署钩子改为#!/bin/bash # /etc/letsencrypt/renewal-hooks/deploy/gunicorn-reload.sh if [ -f /var/run/gunicorn.pid ]; then kill -HUP $(cat /var/run/gunicorn.pid) logger -t certbot Gunicorn reloaded for new certificate fi为什么说“零中断”Gunicorn 的平滑重载机制是新 worker 先 fork 并完成初始化然后再杀掉旧 worker连接会被无缝迁移到新 worker 上。在证书重启这个场景里你甚至可以理解为“双保险”——新 worker 起来时重新加载证书旧 worker 退出前还能处理存量连接。实测在几十个并发连接的压力下重载过程没有出现任何连接拒绝。但这里有一个非常容易踩的坑Gunicorn 默认要求应用代码具备 reload 兼容性。如果你的应用里有大量全局状态比如数据库连接池、内存缓存平滑重载会导致新旧 worker 短暂共存全局状态无法跨进程共享可能引发连接数翻倍或者缓存失效。我的经验是大多数 WSGI 应用其实都能容忍这种重载但如果你的应用启动初始化特别重、动辄十秒级别的启动过程HUP 重载期间服务能力会下降这时候重启方案反而更靠谱。3.4 方案三证书版本检查 自动 reload最优雅但需要写代码如果你连 HUP 信号都不想手动触发想让 Gunicorn 自己感知证书文件变化那就得在应用层写一个小工具。思路是每次收到 HTTP 请求时检查磁盘上证书文件的修改时间或哈希值一旦发现和当前进程持有的版本不一致就自觉触发 reload。实现方式其实可以利用 Flask/Django 的 before_request 钩子或者中间件# cert_watcher.py import os import signal import hashlib _cert_path /etc/letsencrypt/live/example.com/fullchain.pem _cert_hash None _pid os.getpid() def cert_changed(): global _cert_hash with open(_cert_path, rb) as f: current_hash hashlib.sha256(f.read()).hexdigest() if _cert_hash is None: _cert_hash current_hash return False return current_hash ! _cert_hash def check_and_reload(): if cert_changed(): # 向 Gunicorn 主进程发送 HUP 信号 with open(/var/run/gunicorn.pid) as f: master_pid int(f.read().strip()) # 只让一个 worker 触发避免重复发送 if os.getpid() master_pid: return os.kill(master_pid, signal.SIGHUP)这个方案很巧妙但有个问题是只能检测到 Gunicorn master 进程和 worker 进程都在同一台机器上的场景并且需要所有 worker 都共享同一个磁盘路径才能准确感知变化。连续两个请求可能由不同 worker 处理第一个 worker 发现变化发信号第二个 worker 也发现变化又发一次HUP 信号重复多了会造成无谓的 worker 重启。我建议在代码里加一个时间戳去重比如 60 秒内只允许触发一次否则生产环境会有“幽灵重启”的困扰。从工程角度看我仍然推荐方案二。certbot 的 deploy hook 是官方指定的扩展点代码层面不需要任何改动可靠性远高于自定义中间件。4. 自动化续期定时任务、权限与监控告警4.1 certbot 续期的正确触发姿势配置好 deploy hook 之后整个自动化链条就是定时任务触发certbot renewcertbot 检查证书是否快过期剩余寿命低于 30 天才会真正续期→ 续期成功 → 执行 deploy hook → Gunicorn 重载。系统定时任务推荐用 systemd timer而不是 cron因为 systemd timer 支持随机延迟、持久性标记和更清晰的日志。创建/etc/systemd/system/certbot-renew.timer[Unit] DescriptionRun certbot renew twice daily [Timer] OnCalendar*-*-* 00:00:00 OnCalendar*-*-* 12:00:00 RandomizedDelaySec300 Persistenttrue [Install] WantedBytimers.target然后创建对应的 service 文件/etc/systemd/system/certbot-renew.service[Unit] DescriptionRenew Lets Encrypt certificates [Service] Typeoneshot ExecStart/usr/bin/certbot renew --quiet --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/gunicorn-reload.sh注意我在这里用了命令行--deploy-hook参数和前面“直接放到 hooks 目录”的方式二选一即可。重复执行的话同一个续期事件会触发两次 deploy hook造成两次 HUP 信号。我的经验是用目录方式因为 certbot 官方文档明确会执行/etc/letsencrypt/renewal-hooks/deploy/下所有可执行文件这样你将来加其他 hook 直接丢文件进去就行不用去改 systemd 配置。启用并查看状态systemctl enable certbot-renew.timer systemctl start certbot-renew.timer systemctl list-timers certbot-renew.timer4.2 用户权限Gunicorn 能读到私钥吗这是 Gunicorn 直连模式下最常见的问题之一。certbot 写入的/etc/letsencrypt/live/目录默认权限是 700属主是 root。如果 Gunicorn 以非 root 用户运行比如gunicorn用户那么它读取privkey.pem时会直接Permission denied。解决办法有三种第一种把 Gunicorn 服务改为以 root 跑 master 进程然后配置--user和--group参数让 worker 降权。Gunicorn 支持 master 进程以 root 启动然后 fork worker 时切换到指定用户。这样一来master负责绑定端口和读证书启动 worker就能顺利读取私钥worker 又以低权限运行避免安全风险。systemd Unit 里这样写[Service] Userroot Grouproot ExecStart/usr/local/bin/gunicorn -w 4 -b 0.0.0.0:443 \ --usergunicorn --groupgunicorn \ --certfile/etc/letsencrypt/live/example.com/fullchain.pem \ --keyfile/etc/letsencrypt/live/example.com/privkey.pem \ app:app第二种把/etc/letsencrypt/live路径的访问权限开放给 gunicorn 用户但为了安全不推荐直接 chmod 777而是用 POSIX ACLsetfacl -m u:gunicorn:rx /etc/letsencrypt setfacl -m u:gunicorn:rx /etc/letsencrypt/live/example.com setfacl -m u:gunicorn:r /etc/letsencrypt/live/example.com/privkey.pem要让这个权限在续期后依然有效因为 certbot 续期时会重建目录我们再把 deploy hook 里加上这几行 setfacl 命令确保持久生效。第三种干脆把证书复制到应用自己的目录比如/var/lib/your-app/certs/然后用一个单独的定时任务把 certbot 生成的证书同步过去再把同步任务放进 deploy hook。这套方案会把证书管理的路径撕成两半不推荐但确实有人这么干。提示不管选哪种方式重点都是确保 Gunicorn worker 进程启动时能顺利读取证书文件。建议部署完成后先手动执行一次systemctl restart gunicorn再看日志确认没有 PermissionError。4.3 测试续期链路dry-run 与强制续期自动化做好后最怕的就是“看起来配好了实际从来没触发过”。在真实生产环境我会强制做一次完整的续期演练宁可让证书替换一次也不要等到 90 天后才发现钩子脚本权限不对。先用 dry-run 模式验证 ACME 验证通道certbot renew --dry-run这个命令会真正连到 Let‘s Encrypt 的测试环境走完整个 ACME 验证流程但不会替换真实证书。只要能输出成功信息就说明域名验证没问题。然后手动触发一次真实续期用来验证 deploy hook 是否执行、Gunicorn 是否成功重载certbot renew --force-renewal --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/gunicorn-reload.sh跑完后立刻检查证书部署openssl s_client -connect example.com:443 -servername example.com /dev/null 2/dev/null | openssl x509 -noout -dates你会看到新的 notAfter 日期同时查看 Gunicorn 日志确认 HUP 信号已触发重载。4.4 日志与监控知道它没坏比知道它有多好更重要自动化续期最大的风险不是“续期失败”而是“续期失败了你根本不知道”。certbot renew 默认静默运行失败时只会往 syslog 丢一条信息。我建议至少做三件事在 deploy hook 里增加日志输出echo $(date) - Certificate renewed, sending HUP to gunicorn /var/log/certbot-renew.log使用 journalctl 查看 systemd timer 的执行结果journalctl -u certbot-renew.service -n 50 --no-pager配置一个外部监控检查每天定时用 openssl 拉取证书有效期如果剩余天数低于 14 天就向团队报警。这可以作为 certbot 机制失效时的兜底防线。5. 常见坑我踩过的和你大概率会踩的5.1 standalone 模式导致续期失败Gunicorn 直连的架构下80 端口几乎总是被应用占着或者被防火墙挡住。如果你安装 certbot 时没有指定 webroot默认可能选择 standalone 模式。续期时 certbot 尝试绑定 80 端口发现被占用直接报错退出。解决办法就是改用 webroot 模式一模一样但前提是你的应用能把/.well-known/acme-challenge/路径正确地暴露出来。我记得自己第一次用 Flask 做这个映射时代码里写了个app.route(/.well-known/acme-challenge/filename)返回send_from_directory(/var/www/challenges, filename)很顺利就通过了验证。5.2 HUP 信号发出后证书还是旧的这是最让人抓狂的一种情况deploy hook 日志显示执行了HUP 也发了但 openssl 查有效期还是旧证书。排查思路按顺序先确认 Gunicorn master 进程用的 PID 文件是否新鲜。如果你同时用 systemd 管理 Gunicornsystemd 的ExecReload可能被配置成别的动作而你的 deploy hook 是往 PID 文件发信号但那个 PID 文件可能是旧的或路径不对。另一个常见原因是 Gunicorn worker 进程的 HUP 信号语义问题。Gunicorn 的 master 收到 HUP 后会先重新加载配置然后再 reload worker。如果你的应用里用了 gunicorn 配置文件并且配置文件中写死了证书路径为软链接而 Let‘s Encrypt 的live目录本身就是软链接结构就存在极小的概率 worker reload 时读到的还是同一个 inode 下的旧内容。遇到这种情况最简单的方法是在 deploy hook 里先执行一句sync强制文件系统刷盘然后再发 HUP。5.3 证书报错链不完整问题很多时候你发现 Gunicorn 日志里没有报错但客户端就是提示“证书链不完整无法验证”。原因是你把--certfile指向了cert.pem而不是fullchain.pem。Gunicorn 官方文档里说的certfile是“PEM format of certificate”很多人想当然地就用了cert.pem但cert.pem只包含站点证书中间证书没有被带上。客户端如果缺中间证书现代浏览器会自动尝试抓取或者拒绝而一些命令行工具curl、wget、部分语言的 SDK会直接报错。最优解--certfile/etc/letsencrypt/live/example.com/fullchain.pem。这样站点证书和中间证书一起发给客户端校验链完整。5.4 续期后 Gunicorn 无法启动这种坑大多来自权限。某次 debian 安全更新把/etc/letsencrypt的权限策略改严了或者你手动执行 certbot 时用了 sudo而 Gunicorn 是以普通用户跑的续期后重建的privkey.pem属主变了。Gunicorn 读取私钥失败直接启动失败服务全挂。建议在 deploy hook 里加一步权限自愈#!/bin/bash set -e # 确保 gunicorn 用户能读取证书 if command -v setfacl /dev/null 21; then setfacl -m u:gunicorn:rx /etc/letsencrypt setfacl -m u:gunicorn:rx /etc/letsencrypt/live/example.com setfacl -m u:gunicorn:r /etc/letsencrypt/live/example.com/privkey.pem fi # reload if [ -f /var/run/gunicorn.pid ]; then kill -HUP $(cat /var/run/gunicorn.pid) fi这样每次续期后权限被重置都会在 reload 之前被修正避免服务因权限问题起不来。5.5 定时器没触发但看不出来systemd timer 默认情况下如果上一次服务还在运行它不会追加重叠执行但 oneshot 类型很容易在短时间内跑完你很难意识到底有没有执行。建议设置 Persistenttrue 之后故意改一下系统时间来测试它是否会在下次启动时补齐错过的任务。另外用systemctl list-timers --all | grep certbot检查 next trigger 时间是否在正常推进。6. 实操案例一套可直接抄作业的完整配置假设域名是app.example.com应用是 Flask服务用户是gunicorn系统是 Ubuntu 22.04。完整配置如下。6.1 证书首次申请mkdir -p /var/www/challenges certbot certonly --webroot -w /var/www/challenges -d app.example.com \ --email opsexample.com --agree-tos --no-eff-email如果你的 Flask 应用还没把验证路径映射好这里会失败。在这之前先在应用里加一条路由app.route(/.well-known/acme-challenge/path:filename) def acme_challenge(filename): return send_from_directory(/var/www/challenges, filename)6.2 Gunicorn systemd 单元文件创建/etc/systemd/system/gunicorn.service[Unit] DescriptionGunicorn instance for app.example.com Afternetwork.target [Service] Userroot Grouproot WorkingDirectory/opt/app EnvironmentPATH/opt/app/venv/bin ExecStart/opt/app/venv/bin/gunicorn -w 4 \ -b 0.0.0.0:443 \ --usergunicorn --groupgunicorn \ --pid/run/gunicorn.pid \ --access-logfile /var/log/gunicorn/access.log \ --error-logfile /var/log/gunicorn/error.log \ --certfile/etc/letsencrypt/live/app.example.com/fullchain.pem \ --keyfile/etc/letsencrypt/live/app.example.com/privkey.pem \ app:app [Install] WantedBymulti-user.target注意这里 master 以 root 跑worker 降权为 gunicorn 用户完美避开证书读取权限问题。6.3 deploy hook 脚本创建一整个目录结构mkdir -p /etc/letsencrypt/renewal-hooks/deploy写/etc/letsencrypt/renewal-hooks/deploy/gunicorn-reload.sh#!/bin/bash set -euo pipefail LOG_FILE/var/log/certbot-renew.log PID_FILE/run/gunicorn.pid echo $(date -Iseconds) - Certificate renewal event triggered $LOG_FILE # 修正权限续期后可能被重置 if command -v setfacl /dev/null 21; then setfacl -m u:gunicorn:rx /etc/letsencrypt 2$LOG_FILE setfacl -m u:gunicorn:rx /etc/letsencrypt/live 2$LOG_FILE setfacl -m u:gunicorn:rx /etc/letsencrypt/live/app.example.com 2$LOG_FILE setfacl -m u:gunicorn:r /etc/letsencrypt/live/app.example.com/privkey.pem 2$LOG_FILE fi # 发送 HUP 触发 Gunicorn 热重载 if [ -f $PID_FILE ]; then kill -HUP $(cat $PID_FILE) echo $(date -Iseconds) - Sent HUP to gunicorn master pid $(cat $PID_FILE) $LOG_FILE else echo $(date -Iseconds) - ERROR: gunicorn pid file $PID_FILE not found $LOG_FILE exit 1 fi授权chmod x /etc/letsencrypt/renewal-hooks/deploy/gunicorn-reload.sh然后手动跑一次完整链路做验证certbot renew --force-renewal tail -f /var/log/certbot-renew.log openssl s_client -connect app.example.com:443 -servername app.example.com /dev/null 2/dev/null | openssl x509 -noout -dates6.4 定时任务按前面 4.1 里的 timers 配置写好后systemctl start certbot-renew.timer systemctl enable certbot-renew.timer。至此全链路自动化完成。证书会在剩余 30 天时自动续期续期后自动发 HUP 信号热加载新证书整个过程对用户无感。7. 写在最后的运维体会把 Gunicorn 直连 HTTPS 这套架构搭过一遍之后我最大的体会是证书自动化的难点从来不在 certbot 配置上而在于“进程不会帮你重新读文件”这个心智模型。Nginx 之所以简单是因为它内置了 reload-on-change 的语义Gunicorn 没有这个语义你就要自己补一层通知机制。我个人的偏好是能直接用 Nginx 就绝不直连 Gunicorn省心程度差一个量级。但如果你的项目已经走在 Gunicorn 直连的路上了或者你团队压根没有运维资源去维护 Nginx那 deploy hook HUP 热重载这套组合是我实测下来最稳、成本最低的方案。最后再分享一个小技巧deploy hook 里收到续期事件后不要立刻发 HUP等一两秒钟。Let‘s Encrypt 的 ACME 服务器偶尔会在证书刚签发后的极短时间内更新文件元数据虽然概率极低但等这一下能避免 worker 起来时读到不完整的文件。加一句sleep 2损失微不足道但能避免你在凌晨被报警电话叫醒。
阅读完成 · 觉得有帮助?
咨询建站