This commit is contained in:
张威33321
2026-09-09 15:04:54 +08:00
parent fb87be680d
commit a9ec40a289
22 changed files with 303 additions and 352 deletions

98
acme/README.md Normal file
View File

@@ -0,0 +1,98 @@
# 证书管理服务acme.sh
用宿主机上的 [acme.sh](https://github.com/acmesh-official/acme.sh) 管理 Let's Encrypt 证书:
纯 Shell 实现,不依赖 Python / snap / Docker 容器。
## 为什么换成 acme.sh
之前用 certbot 容器时有个绕不开的别扭点certbot 的 `--deploy-hook` 在**容器内**执行,
而我们要 reload 的 nginx 是**宿主机上的容器**,只能在容器里挂 `docker.sock`(等于把宿主机
root 交给容器),或者退而求其次每天无条件发一次 HUP。
acme.sh 直接跑在宿主机,`--reloadcmd` 天然在宿主机执行,**只在证书真正更新时才 reload**
顺带把 certbot 容器、Dockerfile、docker.sock、compose PATH 问题全部消掉。
## 架构
```
宿主机 容器
───────────────────────────────────── ──────────────────────────
acme.sh~/.acme.sh自带 cron
├─ 写挑战文件 ──▶ /var/www/certbot ──bind mount(ro)──▶ nginx-proxy
├─ 写证书 ──▶ /etc/letsencrypt ──bind mount(ro)──▶ nginx-proxy
└─ 续签后执行 reloadcmd: docker kill -s HUP nginx-proxy
用户 ─▶ frps(80/443) ─▶ frpc ─▶ nginx-proxy(80/8443)
```
## 目录约定
| 宿主机路径 | 用途 |
|---|---|
| `/var/www/certbot` | ACME HTTP-01 挑战文件 |
| `/etc/letsencrypt/live/<CERT_NAME>/` | 证书(默认 CERT_NAME=stonelan.cn |
| `nginx/ssl.d/` | HTTPS server 块,证书就绪后由 `enable-ssl.sh` 写入 |
5 个域名合成**一张 SAN 证书**放在同一个目录nginx 的 5 个 server 块都引用它。
## 使用方法
### 1. 安装
```bash
cd common-server-public/acme
chmod +x install.sh issue.sh renew.sh check-cert.sh
./install.sh
```
### 2. 申请证书(一键引导)
```bash
sudo ./issue.sh
```
依次完成:建目录 → 起 nginx此时无 HTTPS 配置)→ 申请证书 → 安装证书 →
写入 `ssl.d/` 并重载 nginx。
可用环境变量覆盖:
```bash
EMAIL=you@example.com \
CERT_NAME=stonelan.cn \
DOMAINS="fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn" \
sudo -E ./issue.sh
```
### 3. 自动续签
`install.sh` 已让 acme.sh 自己写入 cron**无需额外配置**。
建议再加一条兜底告警(`crontab.example`
```cron
0 8 * * * bash <绝对路径>/check-cert.sh >> /var/log/acme-check.log 2>&1
```
## 故障排查
```bash
acme.sh --list # 已申请的证书
acme.sh --cron --debug 2 # 带调试跑一次续签
./check-cert.sh # 剩余天数
# 确认 nginx 只加载了 HTTP 配置(冷启动时属正常)
docker exec nginx-proxy nginx -T | grep -c "listen 8443"
# ACME 路径可达性
echo probe | sudo tee /var/www/certbot/.well-known/acme-challenge/test
curl -i http://fastgpt.stonelan.cn/.well-known/acme-challenge/test
```
## 注意事项
1. 需要 root或 sudo运行要写 `/etc/letsencrypt``/var/www`
2. nginx 容器 bind mount 的是**宿主机路径**,宿主机上这两个目录必须存在,
否则 docker 会自动创建成 root 所有的空目录
3. 挑战文件需要让容器内 nginx 可读,目录保持 755`issue.sh` 已处理)
4. CA 已显式固定为 Let's Encryptacme.sh 3.x 默认是 ZeroSSL
5. 证书有效期 90 天acme.sh 默认在剩余 30 天内续签

32
acme/check-cert.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
#
# 检查证书剩余有效期,低于阈值时以非 0 退出,便于接入告警。
# 自动续签有可能静默失败,必须有一个独立的检查点。
#
set -euo pipefail
CERT_NAME="${CERT_NAME:-stonelan.cn}"
CERT="${CERT:-/etc/letsencrypt/live/$CERT_NAME/cert.pem}"
WARN_DAYS="${WARN_DAYS:-20}"
if [ ! -f "$CERT" ]; then
echo "证书不存在:$CERT" >&2
exit 1
fi
end="$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)"
end_ts="$(date -d "$end" +%s)"
days=$(( (end_ts - $(date +%s)) / 86400 ))
echo "证书文件:$CERT"
echo "到期时间:$end(剩余 $days 天)"
openssl x509 -subject -issuer -noout -in "$CERT" | sed 's/^/ /'
openssl x509 -text -noout -in "$CERT" \
| grep -A1 "Subject Alternative Name" | sed 's/^/ /'
if [ "$days" -lt "$WARN_DAYS" ]; then
echo "警告:剩余 $days 天,低于阈值 $WARN_DAYS" >&2
exit 1
fi
echo "证书状态正常"

10
acme/crontab.example Normal file
View File

@@ -0,0 +1,10 @@
# acme.sh 安装时会自动写入一条 cron每天执行 ~/.acme.sh/acme.sh --cron
# 续签成功后自动执行 --reloadcmd所以续签本身不需要再配 cron。
#
# 这里只需要一个"有效期检查"作为兜底告警:
# 自动续签可能静默失败,剩余天数低于 20 天时脚本以非 0 退出。
0 8 * * * bash /home/awei/workspace/common-server-public/acme/check-cert.sh >> /var/log/acme-check.log 2>&1
# 如需强制每天显式跑一次续签(一般不必),可加:
# 0 3 * * * bash /home/awei/workspace/common-server-public/acme/renew.sh >> /var/log/acme-renew.log 2>&1

25
acme/install.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
#
# 安装 acme.sh纯 Shell ACME 客户端,不依赖 Python / 容器 / snap
#
set -euo pipefail
EMAIL="${EMAIL:-admin@stonelan.cn}"
ACME="${ACME:-$HOME/.acme.sh/acme.sh}"
if [ -s "$ACME" ]; then
echo "acme.sh 已安装:$ACME"
else
echo "==> 安装 acme.sh"
curl https://get.acme.sh | sh -s email="$EMAIL"
fi
# acme.sh 3.x 默认 CA 是 ZeroSSL这里显式固定为 Let's Encrypt
"$ACME" --set-default-ca --server letsencrypt
echo "==> acme.sh 就绪"
"$ACME" --version
# 安装脚本会自动写入一条 cron每天执行 acme.sh --cron无需再手配续签
echo "==> 已注册的 cron"
crontab -l 2>/dev/null | grep acme.sh || echo " (未找到,请手动添加,参考 crontab.example"

61
acme/issue.sh Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# 首次申请证书并启用 HTTPS完整引导
#
# 1. 准备宿主机目录acme.sh 写挑战文件nginx 容器 bind mount 只读
# 2. 启动 nginx —— 此时 ssl.d/ 为空,只加载 80 端口配置,不依赖证书
# 3. 申请 SAN 证书webroot 验证,挑战文件由 nginx 直接对外提供)
# 4. 把证书安装到 /etc/letsencrypt/live/<CERT_NAME>/
# --reloadcmd 会被 acme.sh 记住,之后每次自动续签都执行
# 5. 启用 HTTPS server 块并重载 nginx
#
# 需要 root 权限(要写 /etc/letsencrypt 与 /var/wwwsudo bash issue.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NGINX_DIR="$SCRIPT_DIR/../nginx"
EMAIL="${EMAIL:-admin@stonelan.cn}"
CERT_NAME="${CERT_NAME:-stonelan.cn}"
WEBROOT="${WEBROOT:-/var/www/certbot}"
CERT_HOME="${CERT_HOME:-/etc/letsencrypt/live/$CERT_NAME}"
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
ACME="${ACME:-$HOME/.acme.sh/acme.sh}"
# acme.sh 的 Main_Domain 就是 -d 列表的第一个,--install-cert 要用它
PRIMARY="${DOMAINS%% *}"
if [ ! -s "$ACME" ]; then
echo "未找到 acme.sh请先执行$SCRIPT_DIR/install.sh" >&2
exit 1
fi
# 1. 目录
mkdir -p "$WEBROOT/.well-known/acme-challenge" "$CERT_HOME"
chmod 755 "$WEBROOT" "$WEBROOT/.well-known" "$WEBROOT/.well-known/acme-challenge"
# 2. nginx无证书也能起来
echo "==> 启动 nginx"
docker compose -f "$NGINX_DIR/docker-compose.yml" up -d
# 3. 申请
args=()
for d in $DOMAINS; do
args+=(-d "$d")
done
echo "==> 申请证书(主域 $PRIMARY,共 $(echo $DOMAINS | wc -w) 个域名):$DOMAINS"
"$ACME" --issue --webroot "$WEBROOT" --server letsencrypt "${args[@]}"
# 4. 安装证书
echo "==> 安装证书到 $CERT_HOME"
"$ACME" --install-cert -d "$PRIMARY" \
--cert-file "$CERT_HOME/cert.pem" \
--key-file "$CERT_HOME/privkey.pem" \
--fullchain-file "$CERT_HOME/fullchain.pem" \
--reloadcmd "docker kill -s HUP nginx-proxy"
# 5. 启用 HTTPS
echo "==> 启用 HTTPS 配置"
exec bash "$NGINX_DIR/enable-ssl.sh"

20
acme/renew.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
#
# 手动触发一次续签检查。
#
# acme.sh 安装时已经自动写入了 cron每天 ~/.acme.sh/acme.sh --cron
# 正常情况下不需要手动执行,这个脚本只是方便排查和立即验证。
# 续签成功时 acme.sh 会自动执行 issue.sh 里登记的 --reloadcmd。
#
set -euo pipefail
ACME="${ACME:-$HOME/.acme.sh/acme.sh}"
if [ ! -s "$ACME" ]; then
echo "未找到 acme.sh请先执行本目录下的 install.sh" >&2
exit 1
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始续签检查"
"$ACME" --cron
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 续签检查完成"

View File

@@ -1,19 +0,0 @@
# Certbot 镜像 - Let's Encrypt 证书管理
#
# 【重要】官方 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

View File

@@ -1,117 +0,0 @@
# 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 张)

View File

@@ -1,34 +0,0 @@
#!/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"

View File

@@ -1,10 +0,0 @@
# Let's Encrypt 自动续签 cron 配置
# 编辑crontab -e
#
# 注意cron 的 PATH 极简,务必用 bash <绝对路径> 调用(脚本内部也会自行补齐 PATH
# 续签LE 官方建议一天两次
0 2,15 * * * bash /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

View File

@@ -1,32 +0,0 @@
services:
certbot:
build: .
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
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:
trim-default:
external: true

View File

@@ -1,52 +0,0 @@
#!/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
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NGINX_DIR="$SCRIPT_DIR/../nginx"
EMAIL="${EMAIL:-admin@stonelan.cn}"
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
args=()
for d in $DOMAINS; do
args+=(-d "$d")
done
cd "$SCRIPT_DIR"
# 1. 共享卷
for v in certbot-certs certbot-webroot certbot-logs; do
docker volume create "$v" >/dev/null
done
# 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 \
--non-interactive \
--agree-tos \
--no-eff-email \
--email "$EMAIL" \
--keep-until-expiring \
"${args[@]}"
# 4. 启用 HTTPS
echo "==> 启用 HTTPS 配置"
exec bash "$NGINX_DIR/enable-ssl.sh"

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
#
# 检查证书续签,并重载 nginx。
#
# 说明certbot renew 无论是否真的续签都返回 0无法用退出码判断。
# 这里不做判断,统一发 HUP —— nginx 的 HUP 是优雅 reload代价极低
# 顺带还能刷新 nginx 对上游的 DNS 缓存。
# (若要用 --deploy-hook钩子是在容器内部执行的就得把 docker.sock
# 挂进容器,等于把宿主机 root 交给容器,不值得。)
#
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)"
cd "$SCRIPT_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log "开始续签检查"
docker compose run --rm certbot renew \
--webroot \
--webroot-path /var/www/certbot \
--quiet \
--no-random-sleep-on-renew
if docker kill -s HUP nginx-proxy 2>/dev/null; then
log "nginx 已重载"
else
log "警告nginx-proxy 未运行,跳过重载" >&2
fi
log "续签检查完成"

View File

@@ -17,8 +17,8 @@ server {
} }
# ACME HTTP-01 验证 # ACME HTTP-01 验证
# certbot 通过 webroot 写入 /var/www/certbot/.well-known/acme-challenge/<token> # acme.sh 通过 webroot 写入 /var/www/certbot/.well-known/acme-challenge/<token>
# 该目录是 certbot-webroot 卷nginx 以只读方式挂载 # 这是宿主机目录nginx 容器以只读方式 bind mount
location /.well-known/acme-challenge/ { location /.well-known/acme-challenge/ {
root /var/www/certbot; root /var/www/certbot;
default_type text/plain; default_type text/plain;

View File

@@ -6,23 +6,16 @@ services:
- "8081:80" - "8081:80"
- "8443:8443" - "8443:8443"
volumes: volumes:
# 证书只读 # 证书:宿主机 acme.sh 写入nginx 只读
- certbot-certs:/etc/letsencrypt:ro - /etc/letsencrypt:/etc/letsencrypt:ro
# ACME 验证目录只读certbot 写入挑战文件nginx 直接对外提供 # ACME HTTP-01 挑战文件:宿主机 acme.sh 写入nginx 只读
- certbot-webroot:/var/www/certbot:ro - /var/www/certbot:/var/www/certbot:ro
# HTTPS server 块:默认为空,证书就绪后由 enable-ssl.sh 写入 # HTTPS server 块:默认为空,证书就绪后由 enable-ssl.sh 写入
- ./ssl.d:/etc/nginx/ssl.d:ro - ./ssl.d:/etc/nginx/ssl.d:ro
restart: unless-stopped restart: unless-stopped
networks: networks:
- trim-default - trim-default
volumes:
# 与 certbot/docker-compose.yml 共用,物理卷名由 certbot 侧固定为同名
certbot-certs:
external: true
certbot-webroot:
external: true
networks: networks:
trim-default: trim-default:
external: true external: true

View File

@@ -1,23 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# 启用 HTTPS 配置:只把"已经拿到证书"的域名对应的 server 块 # 启用 HTTPS 配置:证书就绪后,把 ssl.d.available/ 下的 server 块
# 从 ssl.d.available/ 复制 ssl.d/nginx 挂载并 include 的目录), # 复制 ssl.d/nginx 容器 bind mount 并 include 的目录),校验后优雅重载 nginx。
# 然后校验配置并优雅重载 nginx。
# #
# 冷启动时 ssl.d/ 为空nginx 只加载 conf.d/00-http.conf不引用证书 # 冷启动时 ssl.d/ 为空nginx 只加载 conf.d/00-http.conf不引用证书
# 因此可以在没有证书的情况下正常启动并提供 80 端口 —— 这是打破 # 因此可以在没有证书的情况下启动并提供 80 端口 —— 这是打破
# "先有证书还是先有 nginx" 死锁的关键。 # "先有证书还是先有 nginx" 死锁的关键。
# #
set -euo pipefail set -euo pipefail
# cron / 非登录 shell 下 PATH 极简,显式补齐
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AVAILABLE="$SCRIPT_DIR/ssl.d.available" AVAILABLE="$SCRIPT_DIR/ssl.d.available"
ENABLED="$SCRIPT_DIR/ssl.d" ENABLED="$SCRIPT_DIR/ssl.d"
CERT_NAME="${CERT_NAME:-stonelan.cn}"
CERT_FULL="/etc/letsencrypt/live/$CERT_NAME/fullchain.pem"
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}" DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
# 证书在宿主机上,直接判断即可(不再需要进容器)
if [ ! -f "$CERT_FULL" ]; then
echo "证书尚未生成:$CERT_FULL"
echo "请先执行 acme/issue.sh成功后再运行本脚本"
exit 0
fi
mkdir -p "$ENABLED" mkdir -p "$ENABLED"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
@@ -29,12 +35,6 @@ for d in $DOMAINS; do
continue continue
fi fi
# nginx 容器以只读方式挂载了 certbot-certs 卷,用它判断证书是否就绪
if ! docker compose exec -T nginx-proxy test -f "/etc/letsencrypt/live/$d/fullchain.pem" 2>/dev/null; then
echo "跳过 $d:证书不存在(/etc/letsencrypt/live/$d/fullchain.pem"
continue
fi
if ! cmp -s "$conf" "$ENABLED/$d.conf" 2>/dev/null; then if ! cmp -s "$conf" "$ENABLED/$d.conf" 2>/dev/null; then
cp "$conf" "$ENABLED/$d.conf" cp "$conf" "$ENABLED/$d.conf"
copied+=("$ENABLED/$d.conf") copied+=("$ENABLED/$d.conf")
@@ -43,7 +43,7 @@ for d in $DOMAINS; do
done done
if [ "${#copied[@]}" -eq 0 ]; then if [ "${#copied[@]}" -eq 0 ]; then
echo "没有需要变更的 HTTPS 配置" echo "HTTPS 配置已是最新,无需变更"
exit 0 exit 0
fi fi

View File

@@ -1,12 +1,12 @@
# HTTPS 配置 —— fastgpt.stonelan.cn # HTTPS 配置 —— fastgpt.stonelan.cn
# 在 /etc/letsencrypt/live/fastgpt.stonelan.cn/fullchain.pem 存在时, # 5 个域名共用一张 SAN 证书,统一放在 /etc/letsencrypt/live/stonelan.cn/
# 由 nginx/enable-ssl.sh 复制到 ssl.d/ 才会被 nginx 加载。 # 由 nginx/enable-ssl.sh 在证书就绪后复制到 ssl.d/ 才会被 nginx 加载。
server { server {
listen 8443 ssl; listen 8443 ssl;
server_name fastgpt.stonelan.cn; server_name fastgpt.stonelan.cn;
ssl_certificate /etc/letsencrypt/live/fastgpt.stonelan.cn/fullchain.pem; ssl_certificate /etc/letsencrypt/live/stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fastgpt.stonelan.cn/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/stonelan.cn/privkey.pem;
# SSL 优化配置 # SSL 优化配置
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;

View File

@@ -1,12 +1,13 @@
# HTTPS 配置 —— gitea.stonelan.cn # HTTPS 配置 —— gitea.stonelan.cn
# 仅在证书存在时由 nginx/enable-ssl.sh 复制到 ssl.d/ 才会生效 # 5 个域名共用一张 SAN 证书,统一放在 /etc/letsencrypt/live/stonelan.cn/
# 由 nginx/enable-ssl.sh 在证书就绪后复制到 ssl.d/ 才会生效。
server { server {
listen 8443 ssl; listen 8443 ssl;
server_name gitea.stonelan.cn; server_name gitea.stonelan.cn;
client_max_body_size 200m; client_max_body_size 200m;
ssl_certificate /etc/letsencrypt/live/gitea.stonelan.cn/fullchain.pem; ssl_certificate /etc/letsencrypt/live/stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.stonelan.cn/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

View File

@@ -1,11 +1,12 @@
# HTTPS 配置 —— image.stonelan.cn # HTTPS 配置 —— image.stonelan.cn
# 仅在证书存在时由 nginx/enable-ssl.sh 复制到 ssl.d/ 才会生效 # 5 个域名共用一张 SAN 证书,统一放在 /etc/letsencrypt/live/stonelan.cn/
# 由 nginx/enable-ssl.sh 在证书就绪后复制到 ssl.d/ 才会生效。
server { server {
listen 8443 ssl; listen 8443 ssl;
server_name image.stonelan.cn; server_name image.stonelan.cn;
ssl_certificate /etc/letsencrypt/live/image.stonelan.cn/fullchain.pem; ssl_certificate /etc/letsencrypt/live/stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/image.stonelan.cn/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

View File

@@ -1,12 +1,13 @@
# HTTPS 配置 —— registry.stonelan.cn # HTTPS 配置 —— registry.stonelan.cn
# 仅在证书存在时由 nginx/enable-ssl.sh 复制到 ssl.d/ 才会生效 # 5 个域名共用一张 SAN 证书,统一放在 /etc/letsencrypt/live/stonelan.cn/
# 由 nginx/enable-ssl.sh 在证书就绪后复制到 ssl.d/ 才会生效。
server { server {
listen 8443 ssl; listen 8443 ssl;
server_name registry.stonelan.cn; server_name registry.stonelan.cn;
client_max_body_size 500m; client_max_body_size 500m;
ssl_certificate /etc/letsencrypt/live/registry.stonelan.cn/fullchain.pem; ssl_certificate /etc/letsencrypt/live/stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.stonelan.cn/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

View File

@@ -1,11 +1,12 @@
# HTTPS 配置 —— www.stonelan.cn # HTTPS 配置 —— www.stonelan.cn
# 仅在证书存在时由 nginx/enable-ssl.sh 复制到 ssl.d/ 才会生效 # 5 个域名共用一张 SAN 证书,统一放在 /etc/letsencrypt/live/stonelan.cn/
# 由 nginx/enable-ssl.sh 在证书就绪后复制到 ssl.d/ 才会生效。
server { server {
listen 8443 ssl; listen 8443 ssl;
server_name www.stonelan.cn; server_name www.stonelan.cn;
ssl_certificate /etc/letsencrypt/live/www.stonelan.cn/fullchain.pem; ssl_certificate /etc/letsencrypt/live/stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.stonelan.cn/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

View File

@@ -24,19 +24,25 @@ for conf in conf.d/*.conf; do
sudo cp "$conf" /etc/nginx/conf.d/ sudo cp "$conf" /etc/nginx/conf.d/
done done
# HTTPS 配置:只有证书就绪的域名才启用,避免 nginx 因证书缺失拒绝启动 # HTTPS 配置:证书就绪才启用,避免 nginx 因证书缺失拒绝启动
# 5 个域名共用一张 SAN 证书,统一放在 CERT_NAME 目录下
CERT_NAME="${CERT_NAME:-stonelan.cn}"
CERT_FULL="/etc/letsencrypt/live/$CERT_NAME/fullchain.pem"
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}" DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
if [ -f "$CERT_FULL" ]; then
for d in $DOMAINS; do for d in $DOMAINS; do
src="ssl.d.available/$d.conf" src="ssl.d.available/$d.conf"
[ -f "$src" ] || { echo "跳过 $d:缺少 $src"; continue; } if [ -f "$src" ]; then
if [ -f "/etc/letsencrypt/live/$d/fullchain.pem" ]; then
sudo cp "$src" "/etc/nginx/ssl.d/$d.conf" sudo cp "$src" "/etc/nginx/ssl.d/$d.conf"
echo "已启用 $d" echo "已启用 $d"
else else
echo "跳过 $d证书尚未生成" echo "跳过 $d缺少 $src"
fi fi
done done
else
echo "证书尚未生成($CERT_FULL),跳过 HTTPS 配置"
fi
# 测试并重载配置 # 测试并重载配置
if sudo nginx -t; then if sudo nginx -t; then