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,20 +1,19 @@
# Certbot 镜像 - Let's Encrypt 证书管理 # 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 maintainer="awei"
LABEL description="Let's Encrypt certificate management service" LABEL description="Let's Encrypt certificate management service"
# 安装额外工具 # 调试用工具(如需 DNS-01 验证,在此追加 pip install certbot-dns-xxx
RUN apk add --no-cache curl bash RUN apk add --no-cache curl bash
# 创建必要目录 # 创建必要目录
RUN mkdir -p /var/www/certbot /etc/letsencrypt /var/log/letsencrypt RUN mkdir -p /var/www/certbot /etc/letsencrypt /var/log/letsencrypt
# 设置工作目录
WORKDIR /var/www/certbot 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"]

View File

@@ -1,117 +1,117 @@
# Let's Encrypt 证书管理服务 # Let's Encrypt 证书管理服务
独立的 certbot 服务,用于管理 Let's Encrypt SSL证书的申请自动续签。 独立的 certbot 服务,管理 SSL 证书的申请自动续签。
## 架构说明 ## 架构
``` ```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 用户请求 ──▶ frps(远程 80/443) ──▶ frpc ──▶ nginx-proxy (80 / 8443)
用户请求 │───▶│ frps (远程) │───▶│ frpc (本地)
*.stonelan.cn 80/443端口 │ 80/8443端口 │ ACME 验证 ◀── 直接读 certbot-webroot 卷
└─────────────────┘ └─────────────────┘ ─────────────────┘ HTTPS ── 读 certbot-certs 卷(证书)
HTTPS 配置 ◀── 挂载 ./ssl.d证书就绪后才写入
┌─────────────────────────────────────────────────────────────────┐
│ nginx-proxy 容器 │
│ - 80端口: 处理 ACME 验证请求,转发到 certbot 容器 │
│ - 8443端口: HTTPS 反向代理,使用 Let's Encrypt 证书 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ certbot 容器 │
│ - 处理 ACME HTTP-01 验证 │
│ - 定期续签证书 │
│ - 续签后自动重载 nginx (通过 docker.sock) │
└─────────────────────────────────────────────────────────────────┘
``` ```
## 文件说明 certbot **不再常驻**:它只是一个一次性任务容器,通过 `docker compose run --rm` 执行。
- `Dockerfile` - certbot 镜像定义 ## 为什么 HTTPS 配置要单独放
- `docker-compose.yml` - certbot 容器配置
- `init-cert.sh` - 初始化申请证书脚本 nginx 在加载配置时就要把证书读进内存。如果镜像里直接烤入带 `ssl_certificate`
- `renew-cert.sh` - 自动续签脚本 server 块首次部署时证书不存在nginx 会 `[emerg] cannot load certificate` 直接退出;
- `crontab.example` - cron 配置示例 而申请证书又必须要有 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 ```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
```
# 为每个域名申请证书 脚本依次完成:创建共享卷 → 启动 nginx此时无 HTTPS 配置)→ 申请证书
./init-cert.sh fastgpt.stonelan.cn admin@example.com 把已就绪域名的 HTTPS 配置写入 `ssl.d/` 并重载 nginx。
./init-cert.sh gitea.stonelan.cn admin@example.com
./init-cert.sh image.stonelan.cn admin@example.com 默认 5 个域名合成**一张 SAN 证书**,可用环境变量覆盖:
./init-cert.sh registry.stonelan.cn admin@example.com
./init-cert.sh www.stonelan.cn admin@example.com ```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. 配置自动续签 ### 2. 配置自动续签
```bash ```bash
# 编辑 crontab
crontab -e crontab -e
# 参考 crontab.example
# 添加以下行(每天凌晨 2 点检查续签) # 0 2,15 * * * bash <绝对路径>/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 # 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 ```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` 卷名在 `certbot/docker-compose.yml` 里用 `name:` 固定compose 默认会加项目名前缀,
- 私钥: `/etc/letsencrypt/live/<域名>/privkey.pem` 两个 compose 项目目录不同,不固定会导致 nginx 侧 `external: true` 找不到卷)。
## 端口映射 ## 端口
| 服务 | 容器端口 | 宿主机端口 | 说明 | | 服务 | 容器端口 | 宿主机端口 | 远程端口 |
|------|----------|------------|------| |---|---|---|---|
| nginx | 80 | 8081 | HTTP (ACME验证 + 重定向) | | nginx | 80 | 8081 | 80 |
| nginx | 8443 | 8443 | HTTPS (SSL反向代理) | | nginx | 8443 | 8443 | 443 |
| 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
```
## 故障排查 ## 故障排查
### 查看证书状态
```bash ```bash
docker compose exec certbot certbot certificates # nginx 是否只加载了 HTTP 配置(冷启动预期如此)
``` docker exec nginx-proxy nginx -T | grep -c "listen 8443"
### 查看续签日志 # ACME 验证路径是否可达(申请期间在容器内建个测试文件)
```bash 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 tail -f /var/log/certbot-renew.log
``` ```
### 检查 ACME 验证是否可达
```bash
curl http://fastgpt.stonelan.cn/.well-known/acme-challenge/test
```
## 注意事项 ## 注意事项
1. 确保域名 DNS 已正确解析到 frps 服务器 IP 1. 域名 DNS 解析到 frps 服务器,且 frpc 与 nginx 在同一 Docker 网络 `trim-default`
2. 确保 frpc 和 nginx 容器在同一个 Docker 网络 (`trim-default`) 2. **不要**在命令里重复写 `certbot``docker compose run --rm certbot renew` 才是正确写法
3. 证书有效期 90 天,建议每 60 天续签一次cron 已配置每天检查 (官方镜像 ENTRYPOINT 已经是 certbot写成 `run --rm certbot certbot renew` 会失败
4. 首次申请证书前,确保 nginx 容器已启动并能处理 ACME 验证请求 3. 证书有效期 90 天certbot 默认在剩余 30 天内才真正续签
4. `init-cert.sh` 使用 `--keep-until-expiring`,不要改成 `--force-renewal`
否则每次执行都重签,很容易撞 Let's Encrypt 的限流(同一组域名每周 5 张)

34
certbot/check-cert.sh Normal file
View 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"

View File

@@ -1,7 +1,10 @@
# Let's Encrypt 自动续签 cron 配置 # 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

View File

@@ -1,22 +1,30 @@
services: services:
certbot: certbot:
build: . build: .
container_name: certbot image: common-server-certbot:latest
restart: unless-stopped # 一次性任务容器,不常驻:
# docker compose run --rm certbot certonly ... # 申请
# docker compose run --rm certbot renew # 续签
# 注意:官方镜像 ENTRYPOINT 已经是 certbot命令里不要再写一遍 certbot。
volumes: volumes:
- certbot-webroot:/var/www/certbot - certbot-webroot:/var/www/certbot
- certbot-certs:/etc/letsencrypt - certbot-certs:/etc/letsencrypt
- certbot-logs:/var/log/letsencrypt - certbot-logs:/var/log/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: networks:
- trim-default - trim-default
volumes: volumes:
# 固定物理卷名,供 nginx/docker-compose.yml 以 external 方式引用。
# compose 默认会给卷名加项目名前缀,两个 compose 项目目录不同,
# 不显式指定 name 会导致两侧指向不同的卷。
certbot-webroot: certbot-webroot:
name: certbot-webroot
driver: local driver: local
certbot-certs: certbot-certs:
name: certbot-certs
driver: local driver: local
certbot-logs: certbot-logs:
name: certbot-logs
driver: local driver: local
networks: networks:

View File

@@ -1,35 +1,52 @@
#!/bin/bash #!/usr/bin/env bash
# Let's Encrypt 证书初始化脚本 #
# 用法: ./init-cert.sh <域名> [邮箱] # 申请 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 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EMAIL=${2:-"admin@stonelan.cn"} NGINX_DIR="$SCRIPT_DIR/../nginx"
if [ -z "$DOMAIN" ]; then EMAIL="${EMAIL:-admin@stonelan.cn}"
echo "用法: $0 <域名> [邮箱]" DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
echo "示例: $0 fastgpt.stonelan.cn admin@example.com"
exit 1
fi
echo "开始为域名 $DOMAIN 申请证书..." args=()
for d in $DOMAINS; do
args+=(-d "$d")
done
# 确保 certbot 容器运行 cd "$SCRIPT_DIR"
docker compose up -d
# 等待容器启动 # 1. 共享卷
sleep 5 for v in certbot-certs certbot-webroot certbot-logs; do
docker volume create "$v" >/dev/null
done
# 申请证书(使用 webroot 验证方式 # 2. nginx无证书也能起来
docker compose exec certbot certbot certonly \ 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 \
--webroot-path=/var/www/certbot \ --webroot-path /var/www/certbot \
--email "$EMAIL" \ --non-interactive \
--agree-tos \ --agree-tos \
--no-eff-email \ --no-eff-email \
-d "$DOMAIN" \ --email "$EMAIL" \
--force-renewal --keep-until-expiring \
"${args[@]}"
echo "证书申请完成!" # 4. 启用 HTTPS
echo "证书路径: /etc/letsencrypt/live/$DOMAIN/" echo "==> 启用 HTTPS 配置"
exec bash "$NGINX_DIR/enable-ssl.sh"

View File

@@ -1,27 +1,34 @@
#!/bin/bash #!/usr/bin/env bash
# Let's Encrypt 证书自动续签脚本 #
# 续签后自动重载 nginx # 检查证书续签,并重载 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"
# 执行续签 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
docker compose exec certbot certbot renew \
log "开始续签检查"
docker compose run --rm certbot renew \
--webroot \ --webroot \
--webroot-path=/var/www/certbot \ --webroot-path /var/www/certbot \
--quiet --quiet \
--no-random-sleep-on-renew
# 检查是否有证书被续签 if docker kill -s HUP nginx-proxy 2>/dev/null; then
if [ $? -eq 0 ]; then log "nginx 已重载"
echo "证书续签检查完成"
# 重载 nginx发送 HUP 信号)
echo "重载 nginx 配置..."
docker kill -s HUP nginx-proxy 2>/dev/null || echo "nginx-proxy 容器未运行或不存在"
echo "续签流程完成"
else else
echo "证书续签失败" log "警告nginx-proxy 未运行,跳过重载" >&2
exit 1
fi fi
log "续签检查完成"

View File

@@ -1,27 +1,27 @@
# Nginx Docker image # 使用官方 Nginx 镜像
# 包含自定义配置的反向代理 FROM nginx:09051708
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nginx:alpine # 维护者信息
LABEL maintainer="awei"
LABEL description="HTTP server with SSL support"
LABEL maintainer="StoneLan" # 复制配置文件
LABEL description="Nginx reverse proxy container" # conf.d 只放不依赖证书的常驻配置80 端口ACME 验证 + HTTPS 跳转)
# 复制 nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/nginx.conf
COPY conf.d /etc/nginx/conf.d COPY conf.d /etc/nginx/conf.d
COPY verification.html /usr/share/nginx/html/verification.html
# 复制静态文件 # 创建日志目录与 HTTPS 配置目录
COPY html /usr/share/nginx/html # ssl.d 必须存在nginx.conf 里的 wildcard include 在目录不存在时会报 emerg
RUN mkdir -p /var/log/nginx /etc/nginx/ssl.d
# 创建必要的目录 # 暴露端口
RUN mkdir -p /var/log/nginx && \ EXPOSE 80
mkdir -p /etc/nginx/ssl && \ EXPOSE 8443
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx
EXPOSE 80 443 # 健康检查:/healthz 固定返回 200不跟随 301冷启动阶段也不会误判
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --quiet --tries=1 -O /dev/null http://127.0.0.1/healthz || exit 1
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

30
nginx/conf.d/00-http.conf Normal file
View File

@@ -0,0 +1,30 @@
# HTTP 入口ACME 验证 + HTTPS 跳转
#
# 【关键】这个文件必须常驻,且不能引用任何证书文件。
# 首次部署时证书尚不存在,如果 nginx 加载了带 ssl_certificate 的 server 块,
# 会直接 [emerg] cannot load certificate 退出;而申请证书又必须要有 80 端口可用
# —— 形成死锁。因此所有 HTTPS server 块统一放到 /etc/nginx/ssl.d/
# 由 nginx/enable-ssl.sh 在证书就绪后再启用。
server {
listen 80 default_server;
server_name _;
# 健康检查探针(容器 HEALTHCHECK 使用),不跟随 301
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
# ACME HTTP-01 验证
# certbot 通过 webroot 写入 /var/www/certbot/.well-known/acme-challenge/<token>
# 该目录是 certbot-webroot 卷nginx 以只读方式挂载
location /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
}

View File

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

59
nginx/enable-ssl.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
#
# 启用 HTTPS 配置:只把"已经拿到证书"的域名对应的 server 块
# 从 ssl.d.available/ 复制到 ssl.d/nginx 挂载并 include 的目录),
# 然后校验配置并优雅重载 nginx。
#
# 冷启动时 ssl.d/ 为空nginx 只加载 conf.d/00-http.conf不引用证书
# 因此可以在没有证书的情况下正常启动并提供 80 端口 —— 这是打破
# "先有证书还是先有 nginx" 死锁的关键。
#
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)"
AVAILABLE="$SCRIPT_DIR/ssl.d.available"
ENABLED="$SCRIPT_DIR/ssl.d"
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
mkdir -p "$ENABLED"
cd "$SCRIPT_DIR"
copied=()
for d in $DOMAINS; do
conf="$AVAILABLE/$d.conf"
if [ ! -f "$conf" ]; then
echo "跳过 $d:缺少 $conf"
continue
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
cp "$conf" "$ENABLED/$d.conf"
copied+=("$ENABLED/$d.conf")
echo "已启用 $d"
fi
done
if [ "${#copied[@]}" -eq 0 ]; then
echo "没有需要变更的 HTTPS 配置"
exit 0
fi
if out=$(docker compose exec -T nginx-proxy nginx -t 2>&1); then
echo "$out" | sed 's/^/ /'
docker kill -s HUP nginx-proxy
echo "nginx 已重载HTTPS 配置生效"
else
echo "$out" | sed 's/^/ /'
echo "nginx 配置校验失败,回滚本次变更" >&2
rm -f "${copied[@]}"
exit 1
fi

View File

@@ -1,29 +1,34 @@
user nginx; user nginx;
worker_processes auto; worker_processes auto;
error_log /var/log/nginx/error.log notice; error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;
sendfile on; sendfile on;
#tcp_nopush on; #tcp_nopush on;
keepalive_timeout 65; keepalive_timeout 65;
# HTTPS server 块按需加载:
} # 该目录默认为空nginx 的 wildcard include 匹配 0 个文件时不报错),
# 证书申请成功后由 enable-ssl.sh 写入配置,从而避免冷启动时
# 因证书文件不存在导致 nginx 拒绝启动。
include /etc/nginx/ssl.d/*.conf;
}

View File

@@ -1,18 +1,6 @@
# HTTP -> HTTPS 重定向ACME 验证由独立 certbot 服务处理) # HTTPS 配置 —— fastgpt.stonelan.cn
server { # 仅在 /etc/letsencrypt/live/fastgpt.stonelan.cn/fullchain.pem 存在时,
listen 80; # 由 nginx/enable-ssl.sh 复制到 ssl.d/ 才会被 nginx 加载。
server_name fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn;
location /.well-known/acme-challenge/ {
proxy_pass http://certbot:80;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 配置
server { server {
listen 8443 ssl; listen 8443 ssl;
server_name fastgpt.stonelan.cn; server_name fastgpt.stonelan.cn;

View File

@@ -1,4 +1,5 @@
# HTTPS 配置 # HTTPS 配置 —— gitea.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;

View File

@@ -1,4 +1,5 @@
# HTTPS 配置 # HTTPS 配置 —— image.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;

View File

@@ -1,4 +1,5 @@
# HTTPS 配置 # HTTPS 配置 —— registry.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;

View File

@@ -1,4 +1,5 @@
# HTTPS 配置 # HTTPS 配置 —— www.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;

2
nginx/ssl.d/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# nginx 从这里加载 HTTPS 配置wildcard include目录为空时 nginx 正常启动)。
# 首次申请证书前本目录为空,由 ../enable-ssl.sh 按需写入。

View File

@@ -1,4 +1,8 @@
#!/bin/bash #!/usr/bin/env bash
#
# 把仓库里的 nginx 配置同步到宿主机 nginx非容器部署路径
#
set -euo pipefail
# 检查并创建nginx用户 # 检查并创建nginx用户
if ! id -u nginx >/dev/null 2>&1; then if ! id -u nginx >/dev/null 2>&1; then
@@ -6,31 +10,49 @@ if ! id -u nginx >/dev/null 2>&1; then
sudo useradd -r -s /sbin/nologin nginx sudo useradd -r -s /sbin/nologin nginx
fi fi
# 创建 conf.d 目录(如果不存在 # 创建配置目录ssl.d 必须存在nginx.conf 里的 wildcard include 在目录缺失时会报 emerg
sudo mkdir -p /etc/nginx/conf.d sudo mkdir -p /etc/nginx/conf.d /etc/nginx/ssl.d
# 备份并覆盖 nginx.conf 文件 # 备份并覆盖 nginx.conf 文件
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
sudo cp nginx.conf /etc/nginx/ sudo cp nginx.conf /etc/nginx/
# 备份并覆盖 conf.d 目录下的所有 .conf 文件 # 备份并覆盖 conf.d 目录下的所有 .conf 文件(只放不依赖证书的配置)
for conf in conf.d/*.conf; do for conf in conf.d/*.conf; do
filename=$(basename "$conf") filename=$(basename "$conf")
sudo cp "/etc/nginx/conf.d/$filename" "/etc/nginx/conf.d/${filename}.bak" 2>/dev/null || true sudo cp "/etc/nginx/conf.d/$filename" "/etc/nginx/conf.d/${filename}.bak" 2>/dev/null || true
sudo cp "$conf" /etc/nginx/conf.d/ sudo cp "$conf" /etc/nginx/conf.d/
done done
# HTTPS 配置:只有证书已就绪的域名才启用,避免 nginx 因证书缺失拒绝启动
DOMAINS="${DOMAINS:-fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn}"
for d in $DOMAINS; do
src="ssl.d.available/$d.conf"
[ -f "$src" ] || { echo "跳过 $d:缺少 $src"; continue; }
if [ -f "/etc/letsencrypt/live/$d/fullchain.pem" ]; then
sudo cp "$src" "/etc/nginx/ssl.d/$d.conf"
echo "已启用 $d"
else
echo "跳过 $d:证书尚未生成"
fi
done
# 测试并重载配置 # 测试并重载配置
if sudo nginx -t; then if sudo nginx -t; then
sudo systemctl reload nginx if sudo systemctl is-active --quiet nginx; then
echo "配置已生效HTTPS已启用" sudo systemctl reload nginx
echo "配置已重载"
else
sudo systemctl start nginx
echo "nginx 已启动"
fi
else else
echo "配置测试失败,已恢复备份" echo "配置测试失败,已恢复备份"
# 恢复 nginx.conf 备份
sudo cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf sudo cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf
# 恢复 conf.d 目录下的所有 .conf 文件备份
for conf in conf.d/*.conf; do for conf in conf.d/*.conf; do
filename=$(basename "$conf") filename=$(basename "$conf")
sudo mv -f "/etc/nginx/conf.d/${filename}.bak" "/etc/nginx/conf.d/$filename" 2>/dev/null || true sudo mv -f "/etc/nginx/conf.d/${filename}.bak" "/etc/nginx/conf.d/$filename" 2>/dev/null || true
done done
fi exit 1
fi