0908
This commit is contained in:
@@ -1,20 +1,19 @@
|
||||
# Certbot 镜像 - Let's Encrypt 证书管理
|
||||
FROM certbot/certbot:latest
|
||||
#
|
||||
# 【重要】官方 certbot/certbot 镜像的 ENTRYPOINT 是 ["certbot"],这里不要覆盖
|
||||
# ENTRYPOINT 或 CMD。一旦覆盖,容器启动命令会变成 `certbot <你的 CMD>`,
|
||||
# certbot 无法解析而退出。正确用法是把容器当一次性任务跑:
|
||||
# docker compose run --rm certbot certonly ...
|
||||
# docker compose run --rm certbot renew
|
||||
FROM certbot/certbot:v5.8.0
|
||||
|
||||
LABEL maintainer="awei"
|
||||
LABEL description="Let's Encrypt certificate management service"
|
||||
|
||||
# 安装额外工具
|
||||
# 调试用工具(如需 DNS-01 验证,在此追加 pip install certbot-dns-xxx)
|
||||
RUN apk add --no-cache curl bash
|
||||
|
||||
# 创建必要目录
|
||||
RUN mkdir -p /var/www/certbot /etc/letsencrypt /var/log/letsencrypt
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /var/www/certbot
|
||||
|
||||
# 暴露端口(用于 ACME 验证)
|
||||
EXPOSE 80
|
||||
|
||||
# 默认命令:启动 web 服务器用于 ACME 验证
|
||||
CMD ["sh", "-c", "while true; do echo 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nOK' | nc -l -p 80; done"]
|
||||
|
||||
@@ -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 张)
|
||||
|
||||
34
certbot/check-cert.sh
Normal file
34
certbot/check-cert.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 查看证书状态;任一证书剩余有效期低于阈值时以非 0 退出,便于接入告警。
|
||||
# 证书过期是静默事故,光有续签日志不够,必须有一个独立的检查点。
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WARN_DAYS="${WARN_DAYS:-20}"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
if ! out=$(docker compose run --rm certbot certificates 2>&1); then
|
||||
echo "$out"
|
||||
echo "获取证书信息失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$out"
|
||||
|
||||
# certbot 输出形如:Expiry Date: 2026-12-07 02:41:23+00:00 (VALID: 89 days)
|
||||
min="$(echo "$out" | grep -o 'VALID: [0-9]*' | awk '{print $2}' | sort -n | head -1 || true)"
|
||||
|
||||
if [ -z "$min" ]; then
|
||||
echo "未能解析证书有效期" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$min" -lt "$WARN_DAYS" ]; then
|
||||
echo "警告:证书最少剩余 $min 天,低于阈值 $WARN_DAYS 天" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "证书状态正常,最少剩余 $min 天"
|
||||
@@ -1,7 +1,10 @@
|
||||
# Let's Encrypt 自动续签 cron 配置
|
||||
# 每天凌晨 2 点执行续签检查
|
||||
# 编辑:crontab -e
|
||||
#
|
||||
# 注意:cron 的 PATH 极简,务必用 bash <绝对路径> 调用(脚本内部也会自行补齐 PATH)
|
||||
|
||||
# 编辑 crontab: crontab -e
|
||||
# 添加以下行:
|
||||
# 续签:LE 官方建议一天两次
|
||||
0 2,15 * * * bash /home/awei/workspace/common-server-public/certbot/renew-cert.sh >> /var/log/certbot-renew.log 2>&1
|
||||
|
||||
0 2 * * * cd /home/awei/workspace/common-server-public/certbot && ./renew-cert.sh >> /var/log/certbot-renew.log 2>&1
|
||||
# 有效期检查:低于 20 天时脚本以非 0 退出,可配合告警
|
||||
0 8 * * * bash /home/awei/workspace/common-server-public/certbot/check-cert.sh >> /var/log/certbot-check.log 2>&1
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
services:
|
||||
certbot:
|
||||
build: .
|
||||
container_name: certbot
|
||||
restart: unless-stopped
|
||||
image: common-server-certbot:latest
|
||||
# 一次性任务容器,不常驻:
|
||||
# docker compose run --rm certbot certonly ... # 申请
|
||||
# docker compose run --rm certbot renew # 续签
|
||||
# 注意:官方镜像 ENTRYPOINT 已经是 certbot,命令里不要再写一遍 certbot。
|
||||
volumes:
|
||||
- certbot-webroot:/var/www/certbot
|
||||
- certbot-certs:/etc/letsencrypt
|
||||
- certbot-logs:/var/log/letsencrypt
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- trim-default
|
||||
|
||||
volumes:
|
||||
# 固定物理卷名,供 nginx/docker-compose.yml 以 external 方式引用。
|
||||
# compose 默认会给卷名加项目名前缀,两个 compose 项目目录不同,
|
||||
# 不显式指定 name 会导致两侧指向不同的卷。
|
||||
certbot-webroot:
|
||||
name: certbot-webroot
|
||||
driver: local
|
||||
certbot-certs:
|
||||
name: certbot-certs
|
||||
driver: local
|
||||
certbot-logs:
|
||||
name: certbot-logs
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
|
||||
@@ -1,35 +1,52 @@
|
||||
#!/bin/bash
|
||||
# Let's Encrypt 证书初始化脚本
|
||||
# 用法: ./init-cert.sh <域名> [邮箱]
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 申请 Let's Encrypt 证书(默认 5 个域名合成一张 SAN 证书)。
|
||||
#
|
||||
# 完整引导流程:
|
||||
# 1. 创建共享卷(幂等)—— nginx 以 external 方式引用,必须先存在
|
||||
# 2. 启动 nginx —— 此时 ssl.d/ 为空,只加载 80 端口配置,不依赖证书
|
||||
# 3. 申请证书 —— webroot 写入挑战文件,由 nginx 直接对外提供
|
||||
# 4. 启用 HTTPS —— 证书就绪后把 server 块放进 ssl.d/ 并重载
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
set -e
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
DOMAIN=$1
|
||||
EMAIL=${2:-"admin@stonelan.cn"}
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
NGINX_DIR="$SCRIPT_DIR/../nginx"
|
||||
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
echo "用法: $0 <域名> [邮箱]"
|
||||
echo "示例: $0 fastgpt.stonelan.cn admin@example.com"
|
||||
exit 1
|
||||
fi
|
||||
EMAIL="${EMAIL:-admin@stonelan.cn}"
|
||||
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
|
||||
|
||||
echo "开始为域名 $DOMAIN 申请证书..."
|
||||
args=()
|
||||
for d in $DOMAINS; do
|
||||
args+=(-d "$d")
|
||||
done
|
||||
|
||||
# 确保 certbot 容器运行
|
||||
docker compose up -d
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 等待容器启动
|
||||
sleep 5
|
||||
# 1. 共享卷
|
||||
for v in certbot-certs certbot-webroot certbot-logs; do
|
||||
docker volume create "$v" >/dev/null
|
||||
done
|
||||
|
||||
# 申请证书(使用 webroot 验证方式)
|
||||
docker compose exec certbot certbot certonly \
|
||||
# 2. nginx(无证书也能起来)
|
||||
docker compose -f "$NGINX_DIR/docker-compose.yml" up -d
|
||||
|
||||
# 3. 申请证书
|
||||
# --keep-until-expiring:证书离到期还远就跳过,避免撞 LE 的重复证书限流
|
||||
# (原脚本的 --force-renewal 每次都强制重签,同一组域名每周只有 5 次额度)
|
||||
echo "==> 申请证书:$DOMAINS"
|
||||
docker compose run --rm certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email "$EMAIL" \
|
||||
--webroot-path /var/www/certbot \
|
||||
--non-interactive \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
-d "$DOMAIN" \
|
||||
--force-renewal
|
||||
--email "$EMAIL" \
|
||||
--keep-until-expiring \
|
||||
"${args[@]}"
|
||||
|
||||
echo "证书申请完成!"
|
||||
echo "证书路径: /etc/letsencrypt/live/$DOMAIN/"
|
||||
# 4. 启用 HTTPS
|
||||
echo "==> 启用 HTTPS 配置"
|
||||
exec bash "$NGINX_DIR/enable-ssl.sh"
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
#!/bin/bash
|
||||
# Let's Encrypt 证书自动续签脚本
|
||||
# 续签后自动重载 nginx
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 检查证书续签,并重载 nginx。
|
||||
#
|
||||
# 说明:certbot renew 无论是否真的续签都返回 0,无法用退出码判断。
|
||||
# 这里不做判断,统一发 HUP —— nginx 的 HUP 是优雅 reload,代价极低,
|
||||
# 顺带还能刷新 nginx 对上游的 DNS 缓存。
|
||||
# (若要用 --deploy-hook,钩子是在容器内部执行的,就得把 docker.sock
|
||||
# 挂进容器,等于把宿主机 root 交给容器,不值得。)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
set -e
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
echo "开始检查证书续签..."
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 执行续签
|
||||
docker compose exec certbot certbot renew \
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||
|
||||
log "开始续签检查"
|
||||
|
||||
docker compose run --rm certbot renew \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--quiet
|
||||
--webroot-path /var/www/certbot \
|
||||
--quiet \
|
||||
--no-random-sleep-on-renew
|
||||
|
||||
# 检查是否有证书被续签
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "证书续签检查完成"
|
||||
|
||||
# 重载 nginx(发送 HUP 信号)
|
||||
echo "重载 nginx 配置..."
|
||||
docker kill -s HUP nginx-proxy 2>/dev/null || echo "nginx-proxy 容器未运行或不存在"
|
||||
|
||||
echo "续签流程完成"
|
||||
if docker kill -s HUP nginx-proxy 2>/dev/null; then
|
||||
log "nginx 已重载"
|
||||
else
|
||||
echo "证书续签失败"
|
||||
exit 1
|
||||
log "警告:nginx-proxy 未运行,跳过重载" >&2
|
||||
fi
|
||||
|
||||
log "续签检查完成"
|
||||
|
||||
Reference in New Issue
Block a user