Files
common-server-public/certbot
张威33321 fb87be680d 0908
2026-09-08 20:36:17 +08:00
..
2026-09-08 20:36:17 +08:00
2026-09-08 20:36:17 +08:00
2026-09-08 20:36:17 +08:00
2026-09-08 20:36:17 +08:00
2026-09-08 20:36:17 +08:00
2026-09-08 20:36:17 +08:00
2026-09-08 20:36:17 +08:00

Let's Encrypt 证书管理服务

独立的 certbot 服务,管理 SSL 证书的申请与自动续签。

架构

用户请求 ──▶ frps(远程 80/443) ──▶ frpc ──▶ nginx-proxy (80 / 8443)
                                              │
                    ACME 验证 ◀── 直接读 certbot-webroot 卷
                    HTTPS    ◀── 读 certbot-certs 卷(证书)
                    HTTPS 配置 ◀── 挂载 ./ssl.d证书就绪后才写入

certbot 不再常驻:它只是一个一次性任务容器,通过 docker compose run --rm 执行。

为什么 HTTPS 配置要单独放

nginx 在加载配置时就要把证书读进内存。如果镜像里直接烤入带 ssl_certificate 的 server 块首次部署时证书不存在nginx 会 [emerg] cannot load certificate 直接退出; 而申请证书又必须要有 80 端口 —— 形成死锁。

所以拆成两段:

目录 内容 何时加载
nginx/conf.d/00-http.conf 80 端口ACME 验证 + 301 跳转 + /healthz 烤进镜像,始终加载
nginx/ssl.d.available/*.conf 8443 端口的 HTTPS server 块(模板) 不直接加载
nginx/ssl.d/*.conf enable-ssl.sh 从上面复制过来 挂进容器,证书就绪后才有内容

nginx.conf 末尾的 include /etc/nginx/ssl.d/*.conf; 在目录为空时不报错, 因此冷启动阶段 nginx 只加载 80 端口配置,可以正常启动。

使用方法

1. 首次申请证书(一键引导)

cd common-server-public/certbot
chmod +x init-cert.sh renew-cert.sh check-cert.sh ../nginx/enable-ssl.sh
./init-cert.sh

脚本依次完成:创建共享卷 → 启动 nginx此时无 HTTPS 配置)→ 申请证书 → 把已就绪域名的 HTTPS 配置写入 ssl.d/ 并重载 nginx。

默认 5 个域名合成一张 SAN 证书,可用环境变量覆盖:

EMAIL=you@example.com \
DOMAINS="fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn" \
./init-cert.sh

2. 配置自动续签

crontab -e
# 参考 crontab.example
#   0 2,15 * * * bash <绝对路径>/renew-cert.sh  >> /var/log/certbot-renew.log 2>&1
#   0 8   * * * bash <绝对路径>/check-cert.sh   >> /var/log/certbot-check.log 2>&1
  • renew-cert.sh:续签,然后给 nginx 发 HUP 优雅重载
  • check-cert.sh:剩余有效期低于 20 天时以非 0 退出,用于接告警

3. 手动操作

./renew-cert.sh            # 手动续签
./check-cert.sh            # 查看证书与剩余天数
../nginx/enable-ssl.sh     # 单独启用 HTTPS 配置(部分域名没证书时会跳过)
docker compose run --rm certbot certificates

目录与卷

卷 / 目录 容器内路径 使用者 说明
certbot-certs /etc/letsencrypt certbot 读写、nginx 只读 证书
certbot-webroot /var/www/certbot certbot 写、nginx 只读 ACME 挑战文件
certbot-logs /var/log/letsencrypt certbot 日志
./nginx/ssl.d /etc/nginx/ssl.d nginx 只读 HTTPS server 块

卷名在 certbot/docker-compose.yml 里用 name: 固定compose 默认会加项目名前缀, 两个 compose 项目目录不同,不固定会导致 nginx 侧 external: true 找不到卷)。

端口

服务 容器端口 宿主机端口 远程端口
nginx 80 8081 80
nginx 8443 8443 443

故障排查

# nginx 是否只加载了 HTTP 配置(冷启动预期如此)
docker exec nginx-proxy nginx -T | grep -c "listen 8443"

# ACME 验证路径是否可达(申请期间在容器内建个测试文件)
docker exec nginx-proxy sh -c 'echo probe > /var/www/certbot/.well-known/acme-challenge/test' 2>/dev/null || true
curl -i http://fastgpt.stonelan.cn/.well-known/acme-challenge/test

# 查看证书 / 续签日志
docker compose run --rm certbot certificates
tail -f /var/log/certbot-renew.log

注意事项

  1. 域名 DNS 需解析到 frps 服务器,且 frpc 与 nginx 在同一 Docker 网络 trim-default
  2. 不要在命令里重复写 certbotdocker compose run --rm certbot renew 才是正确写法 (官方镜像 ENTRYPOINT 已经是 certbot写成 run --rm certbot certbot renew 会失败)
  3. 证书有效期 90 天certbot 默认在剩余 30 天内才真正续签
  4. init-cert.sh 使用 --keep-until-expiring,不要改成 --force-renewal 否则每次执行都重签,很容易撞 Let's Encrypt 的限流(同一组域名每周 5 张)