# 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. 首次申请证书(一键引导) ```bash 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 证书**,可用环境变量覆盖: ```bash EMAIL=you@example.com \ DOMAINS="fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn" \ ./init-cert.sh ``` ### 2. 配置自动续签 ```bash 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. 手动操作 ```bash ./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 | ## 故障排查 ```bash # 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. **不要**在命令里重复写 `certbot`:`docker 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 张)