This commit is contained in:
张威33321
2026-09-08 20:36:17 +08:00
parent 58d65188a1
commit fb87be680d
19 changed files with 398 additions and 212 deletions

View File

@@ -1,117 +1,117 @@
# Let's Encrypt 证书管理服务
独立的 certbot 服务,用于管理 Let's Encrypt SSL证书的申请自动续签。
独立的 certbot 服务,管理 SSL 证书的申请自动续签。
## 架构说明
## 架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
用户请求 │───▶│ frps (远程) │───▶│ frpc (本地)
*.stonelan.cn 80/443端口 │ 80/8443端口 │
└─────────────────┘ └─────────────────┘ ─────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ nginx-proxy 容器 │
│ - 80端口: 处理 ACME 验证请求,转发到 certbot 容器 │
│ - 8443端口: HTTPS 反向代理,使用 Let's Encrypt 证书 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ certbot 容器 │
│ - 处理 ACME HTTP-01 验证 │
│ - 定期续签证书 │
│ - 续签后自动重载 nginx (通过 docker.sock) │
└─────────────────────────────────────────────────────────────────┘
用户请求 ──▶ frps(远程 80/443) ──▶ frpc ──▶ nginx-proxy (80 / 8443)
ACME 验证 ◀── 直接读 certbot-webroot 卷
HTTPS ── 读 certbot-certs 卷(证书)
HTTPS 配置 ◀── 挂载 ./ssl.d证书就绪后才写入
```
## 文件说明
certbot **不再常驻**:它只是一个一次性任务容器,通过 `docker compose run --rm` 执行。
- `Dockerfile` - certbot 镜像定义
- `docker-compose.yml` - certbot 容器配置
- `init-cert.sh` - 初始化申请证书脚本
- `renew-cert.sh` - 自动续签脚本
- `crontab.example` - cron 配置示例
## 为什么 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. 首次申请证书
### 1. 首次申请证书(一键引导)
```bash
cd /home/awei/workspace/common-server-public/certbot
cd common-server-public/certbot
chmod +x init-cert.sh renew-cert.sh check-cert.sh ../nginx/enable-ssl.sh
./init-cert.sh
```
# 为每个域名申请证书
./init-cert.sh fastgpt.stonelan.cn admin@example.com
./init-cert.sh gitea.stonelan.cn admin@example.com
./init-cert.sh image.stonelan.cn admin@example.com
./init-cert.sh registry.stonelan.cn admin@example.com
./init-cert.sh www.stonelan.cn admin@example.com
脚本依次完成:创建共享卷 → 启动 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
crontab -e
# 添加以下行(每天凌晨 2 点检查续签)
0 2 * * * cd /home/awei/workspace/common-server-public/certbot && ./renew-cert.sh >> /var/log/certbot-renew.log 2>&1
# 参考 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
```
### 3. 手动续签测试
- `renew-cert.sh`:续签,然后给 nginx 发 HUP 优雅重载
- `check-cert.sh`:剩余有效期低于 20 天时以非 0 退出,用于接告警
### 3. 手动操作
```bash
cd /home/awei/workspace/common-server-public/certbot
./renew-cert.sh
./renew-cert.sh # 手动续签
./check-cert.sh # 查看证书与剩余天数
../nginx/enable-ssl.sh # 单独启用 HTTPS 配置(部分域名没证书时会跳过)
docker compose run --rm certbot certificates
```
## 证书路径
## 目录与卷
证书存储在 Docker volume `certbot-certs`nginx 容器通过共享 volume 访问:
| 卷 / 目录 | 容器内路径 | 使用者 | 说明 |
|---|---|---|---|
| `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 块 |
- 证书: `/etc/letsencrypt/live/<域名>/fullchain.pem`
- 私钥: `/etc/letsencrypt/live/<域名>/privkey.pem`
卷名在 `certbot/docker-compose.yml` 里用 `name:` 固定compose 默认会加项目名前缀,
两个 compose 项目目录不同,不固定会导致 nginx 侧 `external: true` 找不到卷)。
## 端口映射
## 端口
| 服务 | 容器端口 | 宿主机端口 | 说明 |
|------|----------|------------|------|
| nginx | 80 | 8081 | HTTP (ACME验证 + 重定向) |
| nginx | 8443 | 8443 | HTTPS (SSL反向代理) |
| frpc | 80 | 80 (远程) | HTTP 映射 |
| frpc | 8443 | 443 (远程) | HTTPS 映射 |
## 重启 nginx
续签后会自动通过 `docker kill -s HUP nginx-proxy` 重载 nginx。如需手动重启
```bash
docker kill -s HUP nginx-proxy
# 或
docker restart nginx-proxy
```
| 服务 | 容器端口 | 宿主机端口 | 远程端口 |
|---|---|---|---|
| nginx | 80 | 8081 | 80 |
| nginx | 8443 | 8443 | 443 |
## 故障排查
### 查看证书状态
```bash
docker compose exec certbot certbot certificates
```
# nginx 是否只加载了 HTTP 配置(冷启动预期如此)
docker exec nginx-proxy nginx -T | grep -c "listen 8443"
### 查看续签日志
```bash
# 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
```
### 检查 ACME 验证是否可达
```bash
curl http://fastgpt.stonelan.cn/.well-known/acme-challenge/test
```
## 注意事项
1. 确保域名 DNS 已正确解析到 frps 服务器 IP
2. 确保 frpc 和 nginx 容器在同一个 Docker 网络 (`trim-default`)
3. 证书有效期 90 天,建议每 60 天续签一次cron 已配置每天检查
4. 首次申请证书前,确保 nginx 容器已启动并能处理 ACME 验证请求
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 张)